inquire/prompts/editor/
prompt.rs1use 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}