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}