Skip to main content

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