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