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('y'), KeyModifiers::CONTROL) => Some(Yank),
54                (Char('a'), KeyModifiers::CONTROL) | (Home, KeyModifiers::NONE) => {
55                    Some(GoToStart)
56                }
57                (Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {
58                    Some(GoToEnd)
59                }
60                (Char(c), KeyModifiers::NONE) => Some(InsertChar(c)),
61                (Char(c), KeyModifiers::SHIFT) => Some(InsertChar(c)),
62                (_, _) => None,
63            }
64        }
65        _ => None,
66    }
67}
68
69/// Renders the input UI at the given position with the given width.
70pub fn write<W: Write>(
71    stdout: &mut W,
72    value: &str,
73    cursor: usize,
74    (x, y): (u16, u16),
75    width: u16,
76) -> Result<()> {
77    queue!(stdout, MoveTo(x, y), SetAttribute(CAttribute::NoReverse))?;
78
79    let val_width = width.max(1) as usize - 1;
80    let len = value.chars().count();
81    let start = (len.max(val_width) - val_width).min(cursor);
82    let mut chars = value.chars().skip(start);
83    let mut i = start;
84
85    // Chars before cursor
86    while i < cursor {
87        i += 1;
88        let c = chars.next().unwrap_or(' ');
89        queue!(stdout, Print(c))?;
90    }
91
92    // Cursor
93    i += 1;
94    let c = chars.next().unwrap_or(' ');
95    queue!(
96        stdout,
97        SetAttribute(CAttribute::Reverse),
98        Print(c),
99        SetAttribute(CAttribute::NoReverse)
100    )?;
101
102    // Chars after the cursor
103    while i <= start + val_width {
104        i += 1;
105        let c = chars.next().unwrap_or(' ');
106        queue!(stdout, Print(c))?;
107    }
108
109    Ok(())
110}
111
112/// Import this trait to implement `Input::handle_event()` for crossterm.
113pub trait EventHandler {
114    /// Handle crossterm event.
115    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
116}
117
118impl EventHandler for Input {
119    /// Handle crossterm event.
120    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged> {
121        to_input_request(evt).and_then(|req| self.handle(req))
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use ratatui::crossterm::event::{
129        Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
130    };
131
132    #[test]
133    fn handle_tab() {
134        let evt = Event::Key(KeyEvent {
135            code: KeyCode::Tab,
136            modifiers: KeyModifiers::NONE,
137            kind: KeyEventKind::Press,
138            state: KeyEventState::NONE,
139        });
140
141        let req = to_input_request(&evt);
142
143        assert!(req.is_none());
144    }
145
146    #[test]
147    fn handle_repeat() {
148        let evt = Event::Key(KeyEvent {
149            code: KeyCode::Char('a'),
150            modifiers: KeyModifiers::NONE,
151            kind: KeyEventKind::Repeat,
152            state: KeyEventState::NONE,
153        });
154
155        let req = to_input_request(&evt);
156
157        assert_eq!(req, Some(InputRequest::InsertChar('a')));
158    }
159}