promkit/preset/
readline.rs

1//! Offers functionality for reading input from the user.
2
3use std::collections::HashSet;
4
5use crate::{
6    core::{
7        crossterm::{
8            self,
9            event::Event,
10            style::{Attribute, Attributes, Color, ContentStyle},
11        },
12        render::{Renderer, SharedRenderer},
13        PaneFactory,
14    },
15    preset::Evaluator,
16    suggest::Suggest,
17    validate::{ErrorMessageGenerator, Validator, ValidatorManager},
18    widgets::{
19        listbox::{self, Listbox},
20        text::{self, Text},
21        text_editor::{self, History},
22    },
23    Signal,
24};
25
26pub mod evaluate;
27
28/// Represents the indices of various components in the readline preset.
29#[derive(PartialEq, Eq, PartialOrd, Ord)]
30pub enum Index {
31    Title = 0,
32    Readline = 1,
33    Suggestion = 2,
34    ErrorMessage = 3,
35}
36
37/// Represents the focus state of the readline,
38/// determining which component is currently active for input handling.
39pub enum Focus {
40    Readline,
41    Suggestion,
42}
43
44/// `Readline` struct provides functionality
45/// for reading a single line of input from the user.
46/// It supports various configurations
47/// such as input masking, history, suggestions, and custom styles.
48pub struct Readline {
49    /// Shared renderer for the prompt, allowing for rendering of UI components.
50    pub renderer: Option<SharedRenderer<Index>>,
51    /// Function to evaluate the input events and update the state of the prompt.
52    pub evaluator: Evaluator<Self>,
53    /// Holds the focus state for event handling, determining which component is currently focused.
54    pub focus: Focus,
55    /// Holds a title's renderer state, used for rendering the title section.
56    pub title: text::State,
57    /// Holds a text editor's renderer state, used for rendering the text input area.
58    pub readline: text_editor::State,
59    /// Optional suggest component for autocomplete functionality.
60    pub suggest: Option<Suggest>,
61    /// Holds a suggest box's renderer state, used when rendering suggestions for autocomplete.
62    pub suggestions: listbox::State,
63    /// Optional validator manager for input validation.
64    pub validator: Option<ValidatorManager<str>>,
65    /// Holds an error message's renderer state, used for rendering error messages.
66    pub error_message: text::State,
67}
68
69impl Default for Readline {
70    fn default() -> Self {
71        Self {
72            renderer: None,
73            evaluator: |event, ctx| Box::pin(evaluate::default(event, ctx)),
74            focus: Focus::Readline,
75            title: text::State {
76                style: ContentStyle {
77                    attributes: Attributes::from(Attribute::Bold),
78                    ..Default::default()
79                },
80                ..Default::default()
81            },
82            readline: text_editor::State {
83                texteditor: Default::default(),
84                history: Default::default(),
85                prefix: String::from("❯❯ "),
86                mask: Default::default(),
87                prefix_style: ContentStyle {
88                    foreground_color: Some(Color::DarkGreen),
89                    ..Default::default()
90                },
91                active_char_style: ContentStyle {
92                    background_color: Some(Color::DarkCyan),
93                    ..Default::default()
94                },
95                inactive_char_style: ContentStyle::default(),
96                edit_mode: Default::default(),
97                word_break_chars: HashSet::from([' ']),
98                lines: Default::default(),
99            },
100            suggest: Default::default(),
101            suggestions: listbox::State {
102                listbox: Listbox::from_displayable(Vec::<String>::new()),
103                cursor: String::from("❯ "),
104                active_item_style: Some(ContentStyle {
105                    foreground_color: Some(Color::DarkGrey),
106                    background_color: Some(Color::DarkYellow),
107                    ..Default::default()
108                }),
109                inactive_item_style: Some(ContentStyle {
110                    foreground_color: Some(Color::DarkGrey),
111                    ..Default::default()
112                }),
113                lines: Some(3),
114            },
115            validator: Default::default(),
116            error_message: text::State {
117                text: Default::default(),
118                style: ContentStyle {
119                    foreground_color: Some(Color::DarkRed),
120                    attributes: Attributes::from(Attribute::Bold),
121                    ..Default::default()
122                },
123                lines: None,
124            },
125        }
126    }
127}
128
129#[async_trait::async_trait]
130impl crate::Prompt for Readline {
131    async fn initialize(&mut self) -> anyhow::Result<()> {
132        let size = crossterm::terminal::size()?;
133        self.renderer = Some(SharedRenderer::new(
134            Renderer::try_new_with_panes(
135                [
136                    (Index::Title, self.title.create_pane(size.0, size.1)),
137                    (Index::Readline, self.readline.create_pane(size.0, size.1)),
138                    (
139                        Index::Suggestion,
140                        self.suggestions.create_pane(size.0, size.1),
141                    ),
142                    (
143                        Index::ErrorMessage,
144                        self.error_message.create_pane(size.0, size.1),
145                    ),
146                ],
147                true,
148            )
149            .await?,
150        ));
151        Ok(())
152    }
153
154    async fn evaluate(&mut self, event: &Event) -> anyhow::Result<Signal> {
155        let ret = (self.evaluator)(event, self).await;
156        let size = crossterm::terminal::size()?;
157        self.render(size.0, size.1).await?;
158        ret
159    }
160
161    type Return = String;
162
163    fn finalize(&mut self) -> anyhow::Result<Self::Return> {
164        let ret = self.readline.texteditor.text_without_cursor().to_string();
165
166        // Reset the text editor state for the next prompt.
167        self.readline.texteditor.erase_all();
168
169        Ok(ret)
170    }
171}
172
173impl Readline {
174    /// Sets the title text displayed above the input field.
175    pub fn title<T: AsRef<str>>(mut self, text: T) -> Self {
176        self.title.text = Text::from(text);
177        self
178    }
179
180    /// Sets the style for the title text.
181    pub fn title_style(mut self, style: ContentStyle) -> Self {
182        self.title.style = style;
183        self
184    }
185
186    /// Enables suggestion functionality with the provided `Suggest` instance.
187    pub fn enable_suggest(mut self, suggest: Suggest) -> Self {
188        self.suggest = Some(suggest);
189        self
190    }
191
192    /// Enables history functionality allowing navigation through previous inputs.
193    pub fn enable_history(mut self) -> Self {
194        self.readline.history = Some(History::default());
195        self
196    }
197
198    /// Sets the prefix string displayed before the input text.
199    pub fn prefix<T: AsRef<str>>(mut self, prefix: T) -> Self {
200        self.readline.prefix = prefix.as_ref().to_string();
201        self
202    }
203
204    /// Sets the character used for masking input text, typically used for password fields.
205    pub fn mask(mut self, mask: char) -> Self {
206        self.readline.mask = Some(mask);
207        self
208    }
209
210    /// Sets the style for the prefix string.
211    pub fn prefix_style(mut self, style: ContentStyle) -> Self {
212        self.readline.prefix_style = style;
213        self
214    }
215
216    /// Sets the style for the currently active character in the input field.
217    pub fn active_char_style(mut self, style: ContentStyle) -> Self {
218        self.readline.active_char_style = style;
219        self
220    }
221
222    /// Sets the style for characters that are not currently active in the input field.
223    pub fn inactive_char_style(mut self, style: ContentStyle) -> Self {
224        self.readline.inactive_char_style = style;
225        self
226    }
227
228    /// Sets the edit mode for the text editor, either insert or overwrite.
229    pub fn edit_mode(mut self, mode: text_editor::Mode) -> Self {
230        self.readline.edit_mode = mode;
231        self
232    }
233
234    /// Sets the characters to be for word break.
235    pub fn word_break_chars(mut self, characters: HashSet<char>) -> Self {
236        self.readline.word_break_chars = characters;
237        self
238    }
239
240    /// Sets the number of lines available for rendering the text editor.
241    pub fn text_editor_lines(mut self, lines: usize) -> Self {
242        self.readline.lines = Some(lines);
243        self
244    }
245
246    /// Sets the function to evaluate the input, allowing for custom evaluation logic.
247    pub fn evaluator(mut self, evaluator: Evaluator<Self>) -> Self {
248        self.evaluator = evaluator;
249        self
250    }
251
252    /// Configures a validator for the input with a function to validate the input and another to configure the error message.
253    pub fn validator(
254        mut self,
255        validator: Validator<str>,
256        error_message_generator: ErrorMessageGenerator<str>,
257    ) -> Self {
258        self.validator = Some(ValidatorManager::new(validator, error_message_generator));
259        self
260    }
261
262    /// Render the prompt with the specified width and height.
263    async fn render(&mut self, width: u16, height: u16) -> anyhow::Result<()> {
264        match self.renderer.as_ref() {
265            Some(renderer) => {
266                renderer
267                    .update([
268                        (Index::Title, self.title.create_pane(width, height)),
269                        (Index::Readline, self.readline.create_pane(width, height)),
270                        (
271                            Index::Suggestion,
272                            self.suggestions.create_pane(width, height),
273                        ),
274                        (
275                            Index::ErrorMessage,
276                            self.error_message.create_pane(width, height),
277                        ),
278                    ])
279                    .render()
280                    .await
281            }
282            None => Err(anyhow::anyhow!("Renderer not initialized")),
283        }
284    }
285}