inquire/prompts/editor/
prompt.rs

1use std::{fs, io::Write, path::Path, process};
2
3use tempfile::NamedTempFile;
4
5use crate::{
6    error::InquireResult,
7    formatter::StringFormatter,
8    prompts::prompt::{ActionResult, Prompt},
9    ui::EditorBackend,
10    validator::{ErrorMessage, StringValidator, Validation},
11    Editor, InquireError,
12};
13
14use super::{action::EditorPromptAction, config::EditorConfig};
15
16pub struct EditorPrompt<'a> {
17    message: &'a str,
18    config: EditorConfig,
19    help_message: Option<&'a str>,
20    formatter: StringFormatter<'a>,
21    validators: Vec<Box<dyn StringValidator>>,
22    error: Option<ErrorMessage>,
23    tmp_file: NamedTempFile,
24}
25
26impl<'a> From<&'a str> for Editor<'a> {
27    fn from(val: &'a str) -> Self {
28        Editor::new(val)
29    }
30}
31
32impl<'a> EditorPrompt<'a> {
33    pub fn new(so: Editor<'a>) -> InquireResult<Self> {
34        Ok(Self {
35            message: so.message,
36            config: (&so).into(),
37            help_message: so.help_message,
38            formatter: so.formatter,
39            validators: so.validators,
40            error: None,
41            tmp_file: Self::create_file(so.file_extension, so.predefined_text)?,
42        })
43    }
44
45    fn create_file(
46        file_extension: &str,
47        predefined_text: Option<&str>,
48    ) -> std::io::Result<NamedTempFile> {
49        let mut tmp_file = tempfile::Builder::new()
50            .prefix("tmp-")
51            .suffix(file_extension)
52            .rand_bytes(10)
53            .tempfile()?;
54
55        if let Some(predefined_text) = predefined_text {
56            tmp_file.write_all(predefined_text.as_bytes())?;
57            tmp_file.flush()?;
58        }
59
60        Ok(tmp_file)
61    }
62
63    fn run_editor(&mut self) -> InquireResult<()> {
64        process::Command::new(&self.config.editor_command)
65            .args(&self.config.editor_command_args)
66            .arg(self.tmp_file.path())
67            .spawn()?
68            .wait()?;
69
70        Ok(())
71    }
72
73    fn validate_current_answer(&self) -> InquireResult<Validation> {
74        if self.validators.is_empty() {
75            return Ok(Validation::Valid);
76        }
77
78        let cur_answer = self.cur_answer()?;
79        for validator in &self.validators {
80            match validator.validate(&cur_answer) {
81                Ok(Validation::Valid) => {}
82                Ok(Validation::Invalid(msg)) => return Ok(Validation::Invalid(msg)),
83                Err(err) => return Err(InquireError::Custom(err)),
84            }
85        }
86
87        Ok(Validation::Valid)
88    }
89
90    fn cur_answer(&self) -> InquireResult<String> {
91        let mut submission = fs::read_to_string(self.tmp_file.path())?;
92        let len = submission.trim_end_matches(&['\n', '\r'][..]).len();
93        submission.truncate(len);
94
95        Ok(submission)
96    }
97}
98
99impl<'a, Backend> Prompt<Backend> for EditorPrompt<'a>
100where
101    Backend: EditorBackend,
102{
103    type Config = EditorConfig;
104    type InnerAction = EditorPromptAction;
105    type Output = String;
106
107    fn message(&self) -> &str {
108        self.message
109    }
110
111    fn config(&self) -> &EditorConfig {
112        &self.config
113    }
114
115    fn format_answer(&self, answer: &String) -> String {
116        (self.formatter)(answer)
117    }
118
119    fn submit(&mut self) -> InquireResult<Option<String>> {
120        let answer = match self.validate_current_answer()? {
121            Validation::Valid => Some(self.cur_answer()?),
122            Validation::Invalid(msg) => {
123                self.error = Some(msg);
124                None
125            }
126        };
127
128        Ok(answer)
129    }
130
131    fn handle(&mut self, action: EditorPromptAction) -> InquireResult<ActionResult> {
132        match action {
133            EditorPromptAction::OpenEditor => {
134                self.run_editor()?;
135                Ok(ActionResult::NeedsRedraw)
136            }
137        }
138    }
139
140    fn render(&self, backend: &mut Backend) -> InquireResult<()> {
141        let prompt = &self.message;
142
143        if let Some(err) = &self.error {
144            backend.render_error_message(err)?;
145        }
146
147        let path = Path::new(&self.config.editor_command);
148        let editor_name = path
149            .file_stem()
150            .and_then(|f| f.to_str())
151            .unwrap_or("editor");
152
153        backend.render_prompt(prompt, editor_name)?;
154
155        if let Some(message) = self.help_message {
156            backend.render_help_message(message)?;
157        }
158
159        Ok(())
160    }
161}