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    /// Sets a specific editor to use instead of the default one.
78    #[must_use]
79    pub fn with_editor(self, editor: Editor) -> Self {
80        Self {
81            editor: Some(editor),
82            ..self
83        }
84    }
85    /// Open the default editor and returns what was written in it.
86    ///
87    /// # Errors
88    /// If the editor call fails, or if the temporary file cannot be read or cleaned up,
89    /// or if the editor call fails.
90    pub fn open_editor(&self) -> Result<String, OpenEditorError> {
91        self.edit_string("")
92    }
93    /// Open the default editor and allows editing of a mutable string.
94    ///
95    /// # Errors
96    ///
97    /// If the editor call fails, or if the temporary file cannot be read or cleaned up,
98    /// or if the editor call fails.
99    pub fn edit_string_mut(&self, string: &mut String) -> Result<(), OpenEditorError> {
100        *string = self.edit_string(string)?;
101        Ok(())
102    }
103
104    /// Open the default editor and allows editing of a string which is then returned.
105    ///
106    /// # Errors
107    /// If the editor call fails, or if the temporary file cannot be read or cleaned up,
108    /// or if the editor call fails.
109    pub fn edit_string(&self, string: &str) -> Result<String, OpenEditorError> {
110        let file_path = match &self.file_path {
111            Some(path) => path,
112            None => &{
113                let mut filename = temp_dir();
114                filename.push(String::from("open_editor_tmp_file"));
115                filename
116            },
117        };
118        // Write the initial content to the temporary file
119        std::fs::write(file_path, string).map_err(OpenEditorError::FileManipulationFail)?;
120        self.open_file(file_path)?;
121        let result =
122            std::fs::read_to_string(file_path).map_err(OpenEditorError::FileManipulationFail)?;
123
124        // Clean up the temporary file after reading
125        std::fs::remove_file(file_path).map_err(|_| {
126            OpenEditorError::TempFileCleanupFail(file_path.to_string_lossy().into_owned())
127        })?;
128
129        Ok(result)
130    }
131    /// Opens the specified file in the editor.
132    ///
133    /// # Errors
134    /// This function will return an error if the editor call fails or if the file cannot be read.
135    pub fn open_file(&self, file_path: &Path) -> Result<(), OpenEditorError> {
136        let editor = match &self.editor {
137            Some(editor) => editor,
138            None => &self.get_default_editor()?,
139        };
140
141        // Build the actual Editor Call
142        let editor_call = EditorCall {
143            editor: editor.clone(),
144            file_path: file_path.to_path_buf(),
145            wait: self.wait,
146            line_number: self.line_number,
147            column_number: self.column_number,
148        };
149        editor_call.call()
150    }
151    /// Gets the default editor from the environment variables `VISUAL` or `EDITOR`.
152    fn get_default_editor(&self) -> Result<Editor, OpenEditorError> {
153        self.custom_env_vars
154            .clone()
155            .into_iter()
156            .chain(ENV_VARS.iter().map(|&s| s.to_string()))
157            .filter_map(env::var_os)
158            .filter(|var| !var.is_empty())
159            .map(|v| {
160                let path = Editor::get_full_path(v.clone());
161                (v.into_string().ok(), path)
162            })
163            .filter_map(|(v, path)| v.map(|v| (v, path)))
164            .map(|(v, cmd)| (Editor::new(EditorKind::from(v), cmd)))
165            .next()
166            .ok_or(OpenEditorError::NoEditorFound)
167    }
168}
169/// Represents a call to an editor with specific options.
170struct EditorCall {
171    editor: Editor,
172    file_path: PathBuf,
173    wait: bool,
174    line_number: usize,
175    column_number: usize,
176}
177impl EditorCall {
178    /// Calls the editor with options from the [`EditorCallBuilder`].
179    /// # Errors
180    ///
181    /// This function will return an error if the commands fails to execute or if the editor returns a non-zero exit code.
182    pub fn call(&self) -> Result<(), OpenEditorError> {
183        self.editor.validate_executable()?; // Ensure the editor binary is valid
184        let command = Command::new(&self.editor.binary_path)
185            .args(self.editor.editor_type.get_editor_args(
186                &self.file_path,
187                self.wait,
188                self.line_number,
189                self.column_number,
190            ))
191            .stdin(Stdio::inherit())
192            .stdout(Stdio::inherit())
193            .stderr(Stdio::inherit())
194            .spawn();
195
196        if !self.wait {
197            return Ok(());
198        }
199
200        match command {
201            Ok(output) => {
202                let output = output
203                    .wait_with_output()
204                    .map_err(|e| OpenEditorError::CommandFail { error: e })?;
205                if output.status.success() {
206                    Ok(())
207                } else {
208                    let stderr = String::from_utf8_lossy(&output.stderr);
209                    Err(OpenEditorError::EditorCallError {
210                        exit_code: output.status.code(),
211                        stderr: stderr.to_string(),
212                    })
213                }
214            }
215            Err(e) => Err(OpenEditorError::CommandFail { error: e }),
216        }
217    }
218}