tui_prompts/
prompt.rs

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