inquire/prompts/editor/mod.rs
1mod action;
2mod config;
3mod prompt;
4
5pub use action::*;
6
7use std::{
8 env,
9 ffi::{OsStr, OsString},
10};
11
12use once_cell::sync::Lazy;
13
14use crate::{
15 error::{InquireError, InquireResult},
16 formatter::StringFormatter,
17 prompts::prompt::Prompt,
18 terminal::get_default_terminal,
19 ui::{Backend, EditorBackend, RenderConfig},
20 validator::StringValidator,
21};
22
23use self::prompt::EditorPrompt;
24
25static DEFAULT_EDITOR: Lazy<OsString> = Lazy::new(get_default_editor_command);
26
27/// This prompt is meant for cases where you need the user to write some text that might not fit in a single line, such as long descriptions or commit messages.
28///
29/// This prompt is gated via the `editor` because it depends on the `tempfile` crate.
30///
31/// This prompt's behavior is to ask the user to either open the editor - by pressing the `e` key - or submit the current text - by pressing the `enter` key. The user can freely open and close the editor as they wish, until they either cancel or submit.
32///
33/// The editor opened is set by default to `nano` on Unix environments and `notepad` on Windows environments. Additionally, if there's an editor set in either the `EDITOR` or `VISUAL` environment variables, it is used instead.
34///
35/// If the user presses `esc` while the editor is not open, it will be interpreted as the user canceling (or skipping) the operation, in which case the prompt call will return `Err(InquireError::OperationCanceled)`.
36///
37/// If the user presses `enter` without ever modyfing the temporary file, it will be treated as an empty submission. If this is unwanted behavior, you can control the user input by using validators.
38///
39/// Finally, this prompt allows a great range of customizable options as all others:
40///
41/// - **Prompt message**: Main message when prompting the user for input, `"What is your name?"` in the example above.
42/// - **Help message**: Message displayed at the line below the prompt.
43/// - **Editor command and its args**: If you want to override the selected editor, you can pass over the command and additional args.
44/// - **File extension**: Custom extension for the temporary file, useful as a proxy for proper syntax highlighting for example.
45/// - **Predefined text**: Pre-defined text to be written to the temporary file before the user is allowed to edit it.
46/// - **Validators**: Custom validators to the user's input, displaying an error message if the input does not pass the requirements.
47/// - **Formatter**: Custom formatter in case you need to pre-process the user input before showing it as the final answer.
48/// - By default, a successfully submitted answer is displayed to the user simply as `<received>`.
49#[derive(Clone)]
50pub struct Editor<'a> {
51 /// Message to be presented to the user.
52 pub message: &'a str,
53
54 /// Command to open the editor.
55 pub editor_command: &'a OsStr,
56
57 /// Args to pass to the editor.
58 pub editor_command_args: &'a [&'a OsStr],
59
60 /// Extension of the file opened in the text editor, useful for syntax highlighting.
61 ///
62 /// The dot prefix should be included in the string, e.g. ".rs".
63 pub file_extension: &'a str,
64
65 /// Predefined text to be present on the text file on the text editor.
66 pub predefined_text: Option<&'a str>,
67
68 /// Help message to be presented to the user.
69 pub help_message: Option<&'a str>,
70
71 /// Function that formats the user input and presents it to the user as the final rendering of the prompt.
72 pub formatter: StringFormatter<'a>,
73
74 /// Collection of validators to apply to the user input.
75 ///
76 /// Validators are executed in the order they are stored, stopping at and displaying to the user
77 /// only the first validation error that might appear.
78 ///
79 /// The possible error is displayed to the user one line above the prompt.
80 pub validators: Vec<Box<dyn StringValidator>>,
81
82 /// RenderConfig to apply to the rendered interface.
83 ///
84 /// Note: The default render config considers if the NO_COLOR environment variable
85 /// is set to decide whether to render the colored config or the empty one.
86 ///
87 /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
88 /// config is treated as the only source of truth. If you want to customize colors
89 /// and still support NO_COLOR, you will have to do this on your end.
90 pub render_config: RenderConfig<'a>,
91}
92
93impl<'a> Editor<'a> {
94 /// Default formatter, set to [DEFAULT_STRING_FORMATTER](crate::formatter::DEFAULT_STRING_FORMATTER)
95 pub const DEFAULT_FORMATTER: StringFormatter<'a> = &|_| String::from("<received>");
96
97 /// Default validators added to the [Editor] prompt, none.
98 pub const DEFAULT_VALIDATORS: Vec<Box<dyn StringValidator>> = vec![];
99
100 /// Default help message.
101 pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None;
102
103 /// Creates a [Editor] with the provided message and default options.
104 pub fn new(message: &'a str) -> Self {
105 Self {
106 message,
107 editor_command: &DEFAULT_EDITOR,
108 editor_command_args: &[],
109 file_extension: ".txt",
110 predefined_text: None,
111 help_message: Self::DEFAULT_HELP_MESSAGE,
112 validators: Self::DEFAULT_VALIDATORS,
113 formatter: Self::DEFAULT_FORMATTER,
114 render_config: RenderConfig::default(),
115 }
116 }
117
118 /// Sets the help message of the prompt.
119 pub fn with_help_message(mut self, message: &'a str) -> Self {
120 self.help_message = Some(message);
121 self
122 }
123
124 /// Sets the predefined text to be written into the temporary file.
125 pub fn with_predefined_text(mut self, text: &'a str) -> Self {
126 self.predefined_text = Some(text);
127 self
128 }
129
130 /// Sets the file extension of the temporary file.
131 pub fn with_file_extension(mut self, file_extension: &'a str) -> Self {
132 self.file_extension = file_extension;
133 self
134 }
135
136 /// Sets the command to open the editor.
137 pub fn with_editor_command(mut self, editor_command: &'a OsStr) -> Self {
138 self.editor_command = editor_command;
139 self
140 }
141
142 /// Sets the args for the command to open the editor.
143 pub fn with_args(mut self, args: &'a [&'a OsStr]) -> Self {
144 self.editor_command_args = args;
145 self
146 }
147
148 /// Sets the formatter.
149 pub fn with_formatter(mut self, formatter: StringFormatter<'a>) -> Self {
150 self.formatter = formatter;
151 self
152 }
153
154 /// Adds a validator to the collection of validators. You might want to use this feature
155 /// in case you need to require certain features from the user's answer, such as
156 /// defining a limit of characters.
157 ///
158 /// Validators are executed in the order they are stored, stopping at and displaying to the user
159 /// only the first validation error that might appear.
160 ///
161 /// The possible error is displayed to the user one line above the prompt.
162 pub fn with_validator<V>(mut self, validator: V) -> Self
163 where
164 V: StringValidator + 'static,
165 {
166 self.validators.push(Box::new(validator));
167 self
168 }
169
170 /// Adds the validators to the collection of validators in the order they are given.
171 /// You might want to use this feature in case you need to require certain features
172 /// from the user's answer, such as defining a limit of characters.
173 ///
174 /// Validators are executed in the order they are stored, stopping at and displaying to the user
175 /// only the first validation error that might appear.
176 ///
177 /// The possible error is displayed to the user one line above the prompt.
178 pub fn with_validators(mut self, validators: &[Box<dyn StringValidator>]) -> Self {
179 for validator in validators {
180 #[allow(suspicious_double_ref_op)]
181 self.validators.push(validator.clone());
182 }
183 self
184 }
185
186 /// Sets the provided color theme to this prompt.
187 ///
188 /// Note: The default render config considers if the NO_COLOR environment variable
189 /// is set to decide whether to render the colored config or the empty one.
190 ///
191 /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
192 /// config is treated as the only source of truth. If you want to customize colors
193 /// and still support NO_COLOR, you will have to do this on your end.
194 pub fn with_render_config(mut self, render_config: RenderConfig<'a>) -> Self {
195 self.render_config = render_config;
196 self
197 }
198
199 /// Parses the provided behavioral and rendering options and prompts
200 /// the CLI user for input according to the defined rules.
201 ///
202 /// This method is intended for flows where the user skipping/cancelling
203 /// the prompt - by pressing ESC - is considered normal behavior. In this case,
204 /// it does not return `Err(InquireError::OperationCanceled)`, but `Ok(None)`.
205 ///
206 /// Meanwhile, if the user does submit an answer, the method wraps the return
207 /// type with `Some`.
208 pub fn prompt_skippable(self) -> InquireResult<Option<String>> {
209 match self.prompt() {
210 Ok(answer) => Ok(Some(answer)),
211 Err(InquireError::OperationCanceled) => Ok(None),
212 Err(err) => Err(err),
213 }
214 }
215
216 /// Parses the provided behavioral and rendering options and prompts
217 /// the CLI user for input according to the defined rules.
218 pub fn prompt(self) -> InquireResult<String> {
219 let (input_reader, terminal) = get_default_terminal()?;
220 let mut backend = Backend::new(input_reader, terminal, self.render_config)?;
221 self.prompt_with_backend(&mut backend)
222 }
223
224 pub(crate) fn prompt_with_backend<B: EditorBackend>(
225 self,
226 backend: &mut B,
227 ) -> InquireResult<String> {
228 EditorPrompt::new(self)?.prompt(backend)
229 }
230}
231
232fn get_default_editor_command() -> OsString {
233 let mut default_editor = if cfg!(windows) {
234 String::from("notepad")
235 } else {
236 String::from("nano")
237 };
238
239 if let Ok(editor) = env::var("EDITOR") {
240 if !editor.is_empty() {
241 default_editor = editor;
242 }
243 }
244
245 if let Ok(editor) = env::var("VISUAL") {
246 if !editor.is_empty() {
247 default_editor = editor;
248 }
249 }
250
251 default_editor.into()
252}