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    #[cfg(test)]
48    pub fn new(key: Key) -> Self {
49        Self {
50            key,
51            ..Self::default()
52        }
53    }
54}
55
56impl From<&KeyEvent> for Input {
57    fn from(event: &KeyEvent) -> Self {
58        let key = match event.code {
59            KeyCode::Char(c) => Key::Char(c),
60            KeyCode::Backspace => Key::Backspace,
61            KeyCode::Enter => Key::Enter,
62            KeyCode::Left => Key::Left,
63            KeyCode::Right => Key::Right,
64            KeyCode::Up => Key::Up,
65            KeyCode::Down => Key::Down,
66            KeyCode::Home => Key::Home,
67            KeyCode::End => Key::End,
68            KeyCode::PageUp => Key::PageUp,
69            KeyCode::PageDown => Key::PageDown,
70            KeyCode::Tab => Key::Tab,
71            KeyCode::BackTab => Key::BackTab,
72            KeyCode::Delete => Key::Delete,
73            KeyCode::Esc => Key::Esc,
74            _ => Key::None,
75        };
76        Self {
77            key,
78            ctrl: event.modifiers.contains(KeyModifiers::CONTROL),
79            alt: event.modifiers.contains(KeyModifiers::ALT),
80            shift: event.modifiers.contains(KeyModifiers::SHIFT),
81        }
82    }
83}
84
85impl From<KeyEvent> for Input {
86    fn from(event: KeyEvent) -> Self {
87        Self::from(&event)
88    }
89}
90
91/// What a key press asks the editor to do.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub(super) enum Action {
94    Insert(char),
95    InsertNewline,
96    InsertTab,
97    DeletePrevChar,
98    DeleteNextChar,
99    DeletePrevWord,
100    DeleteNextWord,
101    DeleteToLineEnd,
102    DeleteToLineStart,
103    /// A cursor movement; the flag says whether it extends a selection.
104    Move(CursorMove, bool),
105    Undo,
106    Redo,
107    Copy,
108    Cut,
109    Paste,
110    /// Nothing this editor handles.
111    Nop,
112}
113
114/// The default key map.
115///
116/// Kept as a free function so it can be unit tested without a buffer.
117pub(super) fn action_for(input: Input) -> Action {
118    let Input {
119        key,
120        ctrl,
121        alt,
122        shift,
123    } = input;
124
125    // Plain (or shifted) printable characters insert themselves.
126    if let Key::Char(c) = key
127        && !ctrl
128        && !alt
129    {
130        return Action::Insert(c);
131    }
132
133    match (key, ctrl, alt) {
134        (Key::Enter, _, _) | (Key::Char('m'), true, false) => Action::InsertNewline,
135        (Key::Tab, false, false) => Action::InsertTab,
136
137        (Key::Backspace, false, false) | (Key::Char('h'), true, false) => Action::DeletePrevChar,
138        (Key::Delete, false, false) | (Key::Char('d'), true, false) => Action::DeleteNextChar,
139        (Key::Backspace, _, true)
140        | (Key::Char('w'), true, false)
141        | (Key::Char('h'), false, true) => Action::DeletePrevWord,
142        (Key::Delete, _, true) | (Key::Char('d'), false, true) => Action::DeleteNextWord,
143        (Key::Char('k'), true, false) => Action::DeleteToLineEnd,
144        (Key::Char('u'), true, false) => Action::DeleteToLineStart,
145        // Ctrl+J is left unmapped: without the keyboard-enhancement protocol
146        // some terminals send Ctrl+Enter, the save chord, as Ctrl+J, and it must
147        // never delete. The field's owner decides what it does.
148        (Key::Char('z'), true, false) => Action::Undo,
149        (Key::Char('r'), true, false) => Action::Redo,
150        // Alt+W, readline's copy: Ctrl+C quits datui from anywhere.
151        (Key::Char('w'), false, true) => Action::Copy,
152        (Key::Char('x'), true, false) => Action::Cut,
153        (Key::Char('y'), true, false) => Action::Paste,
154
155        (Key::Right, true, false) | (Key::Char('f'), false, true) => {
156            Action::Move(CursorMove::WordForward, shift)
157        }
158        (Key::Left, true, false) | (Key::Char('b'), false, true) => {
159            Action::Move(CursorMove::WordBack, shift)
160        }
161        (Key::Right, false, false) | (Key::Char('f'), true, false) => {
162            Action::Move(CursorMove::Forward, shift)
163        }
164        (Key::Left, false, false) | (Key::Char('b'), true, false) => {
165            Action::Move(CursorMove::Back, shift)
166        }
167        (Key::Up, false, false) | (Key::Char('p'), true, false) => {
168            Action::Move(CursorMove::Up, shift)
169        }
170        (Key::Down, false, false) | (Key::Char('n'), true, false) => {
171            Action::Move(CursorMove::Down, shift)
172        }
173        (Key::Home, false, false) | (Key::Char('a'), true, false) => {
174            Action::Move(CursorMove::Head, shift)
175        }
176        (Key::End, false, false) | (Key::Char('e'), true, false) => {
177            Action::Move(CursorMove::End, shift)
178        }
179        (Key::Home, true, _) | (Key::Char('<'), false, true) => {
180            Action::Move(CursorMove::Top, shift)
181        }
182        (Key::End, true, _) | (Key::Char('>'), false, true) => {
183            Action::Move(CursorMove::Bottom, shift)
184        }
185        (Key::PageDown, false, false) | (Key::Char('v'), true, false) => {
186            Action::Move(CursorMove::DownBy(0), shift)
187        }
188        (Key::PageUp, false, false) | (Key::Char('v'), false, true) => {
189            Action::Move(CursorMove::UpBy(0), shift)
190        }
191
192        _ => Action::Nop,
193    }
194}
195
196impl TextArea {
197    /// Apply one key press, returning whether it changed the buffer or the
198    /// cursor.
199    pub fn input(&mut self, input: impl Into<Input>) -> bool {
200        match action_for(input.into()) {
201            Action::Insert(c) => self.insert_char(c),
202            Action::InsertNewline => self.insert_newline(),
203            Action::InsertTab => self.insert_tab(),
204            Action::DeletePrevChar => self.delete_prev_char(),
205            Action::DeleteNextChar => self.delete_next_char(),
206            Action::DeletePrevWord => self.delete_prev_word(),
207            Action::DeleteNextWord => self.delete_next_word(),
208            Action::DeleteToLineEnd => self.delete_line_by_end(),
209            Action::DeleteToLineStart => self.delete_line_by_head(),
210            Action::Move(movement, true) => self.move_cursor_selecting(movement),
211            Action::Move(movement, false) => self.move_cursor(movement),
212            Action::Undo => self.undo(),
213            Action::Redo => self.redo(),
214            Action::Copy => self.copy(),
215            Action::Cut => self.cut(),
216            Action::Paste => self.paste(),
217            Action::Nop => false,
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests;