tui_input/backend/
crossterm.rs

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