Skip to main content

datui_lib/widgets/textarea/
input.rs

1//! Key input handling.
2//!
3//! [`Input`] is a backend-independent key press. Crossterm events convert into
4//! it, and [`TextArea::input`] maps one to an editing action using a default
5//! key map modelled on readline, so the emacs-style bindings terminal users
6//! expect (`Ctrl-A`, `Ctrl-E`, `Ctrl-K`, `Ctrl-W`, ...) work alongside the
7//! arrow keys.
8
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10
11use super::TextArea;
12use super::cursor::CursorMove;
13
14/// A key, independent of any particular terminal backend.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum Key {
17    Char(char),
18    Backspace,
19    Enter,
20    Left,
21    Right,
22    Up,
23    Down,
24    Home,
25    End,
26    PageUp,
27    PageDown,
28    Tab,
29    BackTab,
30    Delete,
31    Esc,
32    /// A key the editor has no use for.
33    #[default]
34    None,
35}
36
37/// A key press together with its modifiers.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub struct Input {
40    pub key: Key,
41    pub ctrl: bool,
42    pub alt: bool,
43    pub shift: bool,
44}
45
46impl Input {
47    pub fn new(key: Key) -> Self {
48        Self {
49            key,
50            ..Self::default()
51        }
52    }
53}
54
55impl From<&KeyEvent> for Input {
56    fn from(event: &KeyEvent) -> Self {
57        let key = match event.code {
58            KeyCode::Char(c) => Key::Char(c),
59            KeyCode::Backspace => Key::Backspace,
60            KeyCode::Enter => Key::Enter,
61            KeyCode::Left => Key::Left,
62            KeyCode::Right => Key::Right,
63            KeyCode::Up => Key::Up,
64            KeyCode::Down => Key::Down,
65            KeyCode::Home => Key::Home,
66            KeyCode::End => Key::End,
67            KeyCode::PageUp => Key::PageUp,
68            KeyCode::PageDown => Key::PageDown,
69            KeyCode::Tab => Key::Tab,
70            KeyCode::BackTab => Key::BackTab,
71            KeyCode::Delete => Key::Delete,
72            KeyCode::Esc => Key::Esc,
73            _ => Key::None,
74        };
75        Self {
76            key,
77            ctrl: event.modifiers.contains(KeyModifiers::CONTROL),
78            alt: event.modifiers.contains(KeyModifiers::ALT),
79            shift: event.modifiers.contains(KeyModifiers::SHIFT),
80        }
81    }
82}
83
84impl From<KeyEvent> for Input {
85    fn from(event: KeyEvent) -> Self {
86        Self::from(&event)
87    }
88}
89
90/// What a key press asks the editor to do.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub(super) enum Action {
93    Insert(char),
94    InsertNewline,
95    InsertTab,
96    DeletePrevChar,
97    DeleteNextChar,
98    DeletePrevWord,
99    DeleteNextWord,
100    DeleteToLineEnd,
101    DeleteToLineStart,
102    /// A cursor movement; the flag says whether it extends a selection.
103    Move(CursorMove, bool),
104    Undo,
105    Redo,
106    Copy,
107    Cut,
108    Paste,
109    /// Nothing this editor handles.
110    Nop,
111}
112
113/// The default key map.
114///
115/// Kept as a free function so it can be unit tested without a buffer.
116pub(super) fn action_for(input: Input) -> Action {
117    let Input {
118        key,
119        ctrl,
120        alt,
121        shift,
122    } = input;
123
124    // Plain (or shifted) printable characters insert themselves.
125    if let Key::Char(c) = key
126        && !ctrl
127        && !alt
128    {
129        return Action::Insert(c);
130    }
131
132    match (key, ctrl, alt) {
133        (Key::Enter, _, _) | (Key::Char('m'), true, false) => Action::InsertNewline,
134        (Key::Tab, false, false) => Action::InsertTab,
135
136        (Key::Backspace, false, false) | (Key::Char('h'), true, false) => Action::DeletePrevChar,
137        (Key::Delete, false, false) | (Key::Char('d'), true, false) => Action::DeleteNextChar,
138        (Key::Backspace, _, true)
139        | (Key::Char('w'), true, false)
140        | (Key::Char('h'), false, true) => Action::DeletePrevWord,
141        (Key::Delete, _, true) | (Key::Char('d'), false, true) => Action::DeleteNextWord,
142        (Key::Char('k'), true, false) => Action::DeleteToLineEnd,
143        (Key::Char('u'), true, false) => Action::DeleteToLineStart,
144        // Ctrl+J is left unmapped: without the keyboard-enhancement protocol
145        // some terminals send Ctrl+Enter, the save chord, as Ctrl+J, and it must
146        // never delete. The field's owner decides what it does.
147        (Key::Char('z'), true, false) => Action::Undo,
148        (Key::Char('r'), true, false) => Action::Redo,
149        // Alt+W, readline's copy: Ctrl+C quits datui from anywhere.
150        (Key::Char('w'), false, true) => Action::Copy,
151        (Key::Char('x'), true, false) => Action::Cut,
152        (Key::Char('y'), true, false) => Action::Paste,
153
154        (Key::Right, true, false) | (Key::Char('f'), false, true) => {
155            Action::Move(CursorMove::WordForward, shift)
156        }
157        (Key::Left, true, false) | (Key::Char('b'), false, true) => {
158            Action::Move(CursorMove::WordBack, shift)
159        }
160        (Key::Right, false, false) | (Key::Char('f'), true, false) => {
161            Action::Move(CursorMove::Forward, shift)
162        }
163        (Key::Left, false, false) | (Key::Char('b'), true, false) => {
164            Action::Move(CursorMove::Back, shift)
165        }
166        (Key::Up, false, false) | (Key::Char('p'), true, false) => {
167            Action::Move(CursorMove::Up, shift)
168        }
169        (Key::Down, false, false) | (Key::Char('n'), true, false) => {
170            Action::Move(CursorMove::Down, shift)
171        }
172        (Key::Home, false, false) | (Key::Char('a'), true, false) => {
173            Action::Move(CursorMove::Head, shift)
174        }
175        (Key::End, false, false) | (Key::Char('e'), true, false) => {
176            Action::Move(CursorMove::End, shift)
177        }
178        (Key::Home, true, _) | (Key::Char('<'), false, true) => {
179            Action::Move(CursorMove::Top, shift)
180        }
181        (Key::End, true, _) | (Key::Char('>'), false, true) => {
182            Action::Move(CursorMove::Bottom, shift)
183        }
184        (Key::PageDown, false, false) | (Key::Char('v'), true, false) => {
185            Action::Move(CursorMove::DownBy(0), shift)
186        }
187        (Key::PageUp, false, false) | (Key::Char('v'), false, true) => {
188            Action::Move(CursorMove::UpBy(0), shift)
189        }
190
191        _ => Action::Nop,
192    }
193}
194
195impl TextArea {
196    /// Apply one key press, returning whether it changed the buffer or the
197    /// cursor.
198    pub fn input(&mut self, input: impl Into<Input>) -> bool {
199        match action_for(input.into()) {
200            Action::Insert(c) => self.insert_char(c),
201            Action::InsertNewline => self.insert_newline(),
202            Action::InsertTab => self.insert_tab(),
203            Action::DeletePrevChar => self.delete_prev_char(),
204            Action::DeleteNextChar => self.delete_next_char(),
205            Action::DeletePrevWord => self.delete_prev_word(),
206            Action::DeleteNextWord => self.delete_next_word(),
207            Action::DeleteToLineEnd => self.delete_line_by_end(),
208            Action::DeleteToLineStart => self.delete_line_by_head(),
209            Action::Move(movement, true) => self.move_cursor_selecting(movement),
210            Action::Move(movement, false) => self.move_cursor(movement),
211            Action::Undo => self.undo(),
212            Action::Redo => self.redo(),
213            Action::Copy => self.copy(),
214            Action::Cut => self.cut(),
215            Action::Paste => self.paste(),
216            Action::Nop => false,
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn key(code: KeyCode, modifiers: KeyModifiers) -> Input {
226        Input::from(KeyEvent::new(code, modifiers))
227    }
228
229    #[test]
230    fn plain_characters_insert_themselves() {
231        assert_eq!(
232            action_for(key(KeyCode::Char('a'), KeyModifiers::NONE)),
233            Action::Insert('a')
234        );
235        assert_eq!(
236            action_for(key(KeyCode::Char('A'), KeyModifiers::SHIFT)),
237            Action::Insert('A')
238        );
239    }
240
241    #[test]
242    fn readline_bindings_match_their_arrow_key_equivalents() {
243        let pairs = [
244            (
245                key(KeyCode::Char('a'), KeyModifiers::CONTROL),
246                key(KeyCode::Home, KeyModifiers::NONE),
247            ),
248            (
249                key(KeyCode::Char('e'), KeyModifiers::CONTROL),
250                key(KeyCode::End, KeyModifiers::NONE),
251            ),
252            (
253                key(KeyCode::Char('f'), KeyModifiers::CONTROL),
254                key(KeyCode::Right, KeyModifiers::NONE),
255            ),
256            (
257                key(KeyCode::Char('b'), KeyModifiers::CONTROL),
258                key(KeyCode::Left, KeyModifiers::NONE),
259            ),
260            (
261                key(KeyCode::Char('h'), KeyModifiers::CONTROL),
262                key(KeyCode::Backspace, KeyModifiers::NONE),
263            ),
264            (
265                key(KeyCode::Char('d'), KeyModifiers::CONTROL),
266                key(KeyCode::Delete, KeyModifiers::NONE),
267            ),
268        ];
269        for (emacs, arrow) in pairs {
270            assert_eq!(
271                action_for(emacs),
272                action_for(arrow),
273                "{emacs:?} vs {arrow:?}"
274            );
275        }
276    }
277
278    #[test]
279    fn ctrl_u_kills_to_line_start_and_ctrl_z_undoes() {
280        assert_eq!(
281            action_for(key(KeyCode::Char('u'), KeyModifiers::CONTROL)),
282            Action::DeleteToLineStart
283        );
284        assert_eq!(
285            action_for(key(KeyCode::Char('z'), KeyModifiers::CONTROL)),
286            Action::Undo
287        );
288    }
289
290    #[test]
291    fn ctrl_j_never_edits() {
292        assert_eq!(
293            action_for(key(KeyCode::Char('j'), KeyModifiers::CONTROL)),
294            Action::Nop
295        );
296    }
297
298    #[test]
299    fn shifted_movement_extends_the_selection() {
300        assert_eq!(
301            action_for(key(KeyCode::Right, KeyModifiers::SHIFT)),
302            Action::Move(CursorMove::Forward, true)
303        );
304        assert_eq!(
305            action_for(key(KeyCode::Right, KeyModifiers::NONE)),
306            Action::Move(CursorMove::Forward, false)
307        );
308    }
309
310    #[test]
311    fn ctrl_arrows_move_by_word() {
312        assert_eq!(
313            action_for(key(KeyCode::Right, KeyModifiers::CONTROL)),
314            Action::Move(CursorMove::WordForward, false)
315        );
316        assert_eq!(
317            action_for(key(KeyCode::Left, KeyModifiers::CONTROL)),
318            Action::Move(CursorMove::WordBack, false)
319        );
320    }
321
322    #[test]
323    fn unmapped_keys_do_nothing() {
324        assert_eq!(
325            action_for(key(KeyCode::Esc, KeyModifiers::NONE)),
326            Action::Nop
327        );
328        assert_eq!(
329            action_for(key(KeyCode::F(5), KeyModifiers::NONE)),
330            Action::Nop
331        );
332        assert_eq!(
333            action_for(key(KeyCode::BackTab, KeyModifiers::SHIFT)),
334            Action::Nop
335        );
336    }
337
338    #[test]
339    fn key_events_convert_with_their_modifiers() {
340        let input = key(
341            KeyCode::Char('k'),
342            KeyModifiers::CONTROL | KeyModifiers::ALT,
343        );
344        assert_eq!(input.key, Key::Char('k'));
345        assert!(input.ctrl);
346        assert!(input.alt);
347        assert!(!input.shift);
348    }
349}