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}