tui_prompts/
prompt.rs

1use std::iter::once;
2
3use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
4use itertools::chain;
5use ratatui_core::layout::Rect;
6use ratatui_core::terminal::Frame;
7use ratatui_core::widgets::StatefulWidget;
8use unicode_width::UnicodeWidthChar;
9
10use crate::Status;
11
12/// A prompt that can be drawn to a terminal.
13pub trait Prompt: StatefulWidget {
14    /// Draws the prompt widget.
15    ///
16    /// This is in addition to the [`StatefulWidget`] trait implementation as we need the [`Frame`]
17    /// to set the cursor position.
18    ///
19    /// [`StatefulWidget`]: ratatui_core::widgets::StatefulWidget
20    /// [`Frame`]: ratatui_core::terminal::Frame
21    fn draw(self, frame: &mut Frame, area: Rect, state: &mut Self::State);
22}
23
24/// The focus state of a prompt.
25#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
26pub enum FocusState {
27    #[default]
28    Unfocused,
29    Focused,
30}
31
32/// The state of a prompt.
33///
34/// Keybindings:
35/// - Enter: Complete
36/// - Esc | Ctrl+C: Abort
37/// - Left | Ctrl+B: Move cursor left
38/// - Right | Ctrl+F: Move cursor right
39/// - Home | Ctrl+A: Move cursor to start of line
40/// - End | Ctrl+E: Move cursor to end of line
41/// - Backspace | Ctrl+H: Delete character before cursor
42/// - Delete | Ctrl+D: Delete character after cursor
43/// - Ctrl+K: Delete from cursor to end of line
44/// - Ctrl+U: Delete from cursor to start of line
45pub trait State {
46    /// The status of the prompt.
47    fn status(&self) -> Status;
48
49    /// A mutable reference to the status of the prompt.
50    fn status_mut(&mut self) -> &mut Status;
51
52    /// A mutable reference to the focus state of the prompt.
53    fn focus_state_mut(&mut self) -> &mut FocusState;
54
55    /// The focus state of the prompt.
56    fn focus_state(&self) -> FocusState;
57
58    /// Sets the focus state of the prompt to [`FocusState::Focused`].
59    fn focus(&mut self) {
60        *self.focus_state_mut() = FocusState::Focused;
61    }
62
63    /// Sets the focus state of the prompt to [`FocusState::Unfocused`].
64    fn blur(&mut self) {
65        *self.focus_state_mut() = FocusState::Unfocused;
66    }
67
68    /// Whether the prompt is focused.
69    fn is_focused(&self) -> bool {
70        self.focus_state() == FocusState::Focused
71    }
72
73    /// The position of the cursor in the prompt.
74    fn position(&self) -> usize;
75
76    /// A mutable reference to the position of the cursor in the prompt.
77    fn position_mut(&mut self) -> &mut usize;
78
79    /// The cursor position of the prompt.
80    fn cursor(&self) -> (u16, u16);
81
82    /// A mutable reference to the cursor position of the prompt.
83    fn cursor_mut(&mut self) -> &mut (u16, u16);
84
85    /// The value of the prompt.
86    fn value(&self) -> &str;
87
88    /// A mutable reference to the value of the prompt.
89    fn value_mut(&mut self) -> &mut String;
90
91    fn len(&self) -> usize {
92        self.value().chars().count()
93    }
94
95    fn is_empty(&self) -> bool {
96        self.value().len() == 0
97    }
98
99    fn handle_key_event(&mut self, key_event: KeyEvent) {
100        if key_event.kind == KeyEventKind::Release {
101            return;
102        }
103
104        match (key_event.code, key_event.modifiers) {
105            (KeyCode::Enter, _) => self.complete(),
106            (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => self.abort(),
107            (KeyCode::Left, _) | (KeyCode::Char('b'), KeyModifiers::CONTROL) => self.move_left(),
108            (KeyCode::Right, _) | (KeyCode::Char('f'), KeyModifiers::CONTROL) => self.move_right(),
109            (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => self.move_start(),
110            (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => self.move_end(),
111            (KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
112                self.backspace();
113            }
114            (KeyCode::Delete, _) | (KeyCode::Char('d'), KeyModifiers::CONTROL) => self.delete(),
115            (KeyCode::Char('k'), KeyModifiers::CONTROL) => self.kill(),
116            (KeyCode::Char('u'), KeyModifiers::CONTROL) => self.truncate(),
117            (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => self.push(c),
118            _ => {}
119        }
120    }
121
122    fn complete(&mut self) {
123        *self.status_mut() = Status::Done;
124    }
125
126    fn abort(&mut self) {
127        *self.status_mut() = Status::Aborted;
128    }
129
130    fn delete(&mut self) {
131        let position = self.position();
132        if position == self.len() {
133            return;
134        }
135        *self.value_mut() = chain!(
136            self.value().chars().take(position),
137            self.value().chars().skip(position + 1)
138        )
139        .collect();
140    }
141
142    fn backspace(&mut self) {
143        let position = self.position();
144        if position == 0 {
145            return;
146        }
147        *self.value_mut() = chain!(
148            self.value().chars().take(position.saturating_sub(1)),
149            self.value().chars().skip(position)
150        )
151        .collect();
152        *self.position_mut() = position.saturating_sub(1);
153    }
154
155    fn move_right(&mut self) {
156        if self.position() == self.len() {
157            return;
158        }
159        *self.position_mut() = self.position().saturating_add(1);
160    }
161
162    fn move_left(&mut self) {
163        *self.position_mut() = self.position().saturating_sub(1);
164    }
165
166    fn move_end(&mut self) {
167        *self.position_mut() = self.len();
168    }
169
170    fn move_start(&mut self) {
171        *self.position_mut() = 0;
172    }
173
174    fn kill(&mut self) {
175        let position = self.position();
176        self.value_mut().truncate(position);
177    }
178
179    fn truncate(&mut self) {
180        self.value_mut().clear();
181        *self.position_mut() = 0;
182    }
183
184    fn push(&mut self, c: char) {
185        if self.position() == self.len() {
186            self.value_mut().push(c);
187        } else {
188            // We cannot use String::insert() as it operates on bytes, which can lead to incorrect
189            // modifications with multibyte characters. Instead, we handle text
190            // manipulation at the character level using Rust's char type for Unicode
191            // correctness. Check docs of String::insert() and String::chars() for futher info.
192            *self.value_mut() = chain![
193                self.value().chars().take(self.position()),
194                once(c),
195                self.value().chars().skip(self.position())
196            ]
197            .collect();
198        }
199        *self.position_mut() = self.position().saturating_add(1);
200    }
201
202    /// The character width of the stored value up to the position pos.
203    fn width_to_pos(&self, pos: usize) -> usize {
204        self.value()
205            .chars()
206            .take(pos)
207            // assign a width of zero to control characters
208            .map(|x| x.width().unwrap_or(0))
209            .sum()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use ratatui_core::style::Stylize;
216
217    use super::*;
218
219    #[test]
220    fn status_symbols() {
221        assert_eq!(Status::Pending.symbol(), "?".cyan());
222        assert_eq!(Status::Aborted.symbol(), "✘".red());
223        assert_eq!(Status::Done.symbol(), "✔".green());
224    }
225}