tui_input/backend/
crossterm.rs

1use crate::{Input, InputRequest, StateChanged};
2use ratatui::crossterm::event::{
3    Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
4};
5use ratatui::crossterm::{
6    cursor::MoveTo,
7    queue,
8    style::{Attribute as CAttribute, Print, SetAttribute},
9};
10use std::io::{Result, Write};
11
12/// Converts crossterm event into input requests.
13pub fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
14    use InputRequest::*;
15    use KeyCode::*;
16    match evt {
17        CrosstermEvent::Key(KeyEvent {
18            code,
19            modifiers,
20            kind,
21            state: _,
22        }) if *kind == KeyEventKind::Press || *kind == KeyEventKind::Repeat => {
23            match (*code, *modifiers) {
24                (Backspace, KeyModifiers::NONE) | (Char('h'), KeyModifiers::CONTROL) => {
25                    Some(DeletePrevChar)
26                }
27                (Delete, KeyModifiers::NONE) => Some(DeleteNextChar),
28                (Tab, KeyModifiers::NONE) => None,
29                (Left, KeyModifiers::NONE) | (Char('b'), KeyModifiers::CONTROL) => {
30                    Some(GoToPrevChar)
31                }
32                (Left, KeyModifiers::CONTROL) | (Char('b'), KeyModifiers::META) => {
33                    Some(GoToPrevWord)
34                }
35                (Right, KeyModifiers::NONE) | (Char('f'), KeyModifiers::CONTROL) => {
36                    Some(GoToNextChar)
37                }
38                (Right, KeyModifiers::CONTROL) | (Char('f'), KeyModifiers::META) => {
39                    Some(GoToNextWord)
40                }
41                (Char('u'), KeyModifiers::CONTROL) => Some(DeleteLine),
42
43                (Char('w'), KeyModifiers::CONTROL)
44                | (Char('d'), KeyModifiers::META)
45                | (Backspace, KeyModifiers::META)
46                | (Backspace, KeyModifiers::ALT) => Some(DeletePrevWord),
47
48                (Delete, KeyModifiers::CONTROL) => Some(DeleteNextWord),
49                (Char('k'), KeyModifiers::CONTROL) => Some(DeleteTillEnd),
50                (Char('a'), KeyModifiers::CONTROL) | (Home, KeyModifiers::NONE) => {
51                    Some(GoToStart)
52                }
53                (Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {
54                    Some(GoToEnd)
55                }
56                (Char(c), KeyModifiers::NONE) => Some(InsertChar(c)),
57                (Char(c), KeyModifiers::SHIFT) => Some(InsertChar(c)),
58                (_, _) => None,
59            }
60        }
61        _ => None,
62    }
63}
64
65/// Renders the input UI at the given position with the given width.
66pub fn write<W: Write>(
67    stdout: &mut W,
68    value: &str,
69    cursor: usize,
70    (x, y): (u16, u16),
71    width: u16,
72) -> Result<()> {
73    queue!(stdout, MoveTo(x, y), SetAttribute(CAttribute::NoReverse))?;
74
75    let val_width = width.max(1) as usize - 1;
76    let len = value.chars().count();
77    let start = (len.max(val_width) - val_width).min(cursor);
78    let mut chars = value.chars().skip(start);
79    let mut i = start;
80
81    // Chars before cursor
82    while i < cursor {
83        i += 1;
84        let c = chars.next().unwrap_or(' ');
85        queue!(stdout, Print(c))?;
86    }
87
88    // Cursor
89    i += 1;
90    let c = chars.next().unwrap_or(' ');
91    queue!(
92        stdout,
93        SetAttribute(CAttribute::Reverse),
94        Print(c),
95        SetAttribute(CAttribute::NoReverse)
96    )?;
97
98    // Chars after the cursor
99    while i <= start + val_width {
100        i += 1;
101        let c = chars.next().unwrap_or(' ');
102        queue!(stdout, Print(c))?;
103    }
104
105    Ok(())
106}
107
108/// Import this trait to implement `Input::handle_event()` for crossterm.
109pub trait EventHandler {
110    /// Handle crossterm event.
111    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
112}
113
114impl EventHandler for Input {
115    /// Handle crossterm event.
116    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged> {
117        to_input_request(evt).and_then(|req| self.handle(req))
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use ratatui::crossterm::event::{KeyEventKind, KeyEventState};
124
125    use super::*;
126
127    #[test]
128    fn handle_tab() {
129        let evt = CrosstermEvent::Key(KeyEvent {
130            code: KeyCode::Tab,
131            modifiers: KeyModifiers::NONE,
132            kind: KeyEventKind::Press,
133            state: KeyEventState::NONE,
134        });
135
136        let req = to_input_request(&evt);
137
138        assert!(req.is_none());
139    }
140
141    #[test]
142    fn handle_repeat() {
143        let evt = CrosstermEvent::Key(KeyEvent {
144            code: KeyCode::Char('a'),
145            modifiers: KeyModifiers::NONE,
146            kind: KeyEventKind::Repeat,
147            state: KeyEventState::NONE,
148        });
149
150        let req = to_input_request(&evt);
151
152        assert_eq!(req, Some(InputRequest::InsertChar('a')));
153    }
154}