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)
63                    if modifiers == KeyModifiers::CONTROL | KeyModifiers::ALT =>
64                {
65                    Some(InsertChar(c))
66                }
67                (_, _) => None,
68            }
69        }
70        _ => None,
71    }
72}
73
74/// Renders the input UI at the given position with the given width.
75pub fn write<W: Write>(
76    stdout: &mut W,
77    value: &str,
78    cursor: usize,
79    (x, y): (u16, u16),
80    width: u16,
81) -> Result<()> {
82    queue!(stdout, MoveTo(x, y), SetAttribute(CAttribute::NoReverse))?;
83
84    let val_width = width.max(1) as usize - 1;
85    let len = value.chars().count();
86    let start = (len.max(val_width) - val_width).min(cursor);
87    let mut chars = value.chars().skip(start);
88    let mut i = start;
89
90    // Chars before cursor
91    while i < cursor {
92        i += 1;
93        let c = chars.next().unwrap_or(' ');
94        queue!(stdout, Print(c))?;
95    }
96
97    // Cursor
98    i += 1;
99    let c = chars.next().unwrap_or(' ');
100    queue!(
101        stdout,
102        SetAttribute(CAttribute::Reverse),
103        Print(c),
104        SetAttribute(CAttribute::NoReverse)
105    )?;
106
107    // Chars after the cursor
108    while i <= start + val_width {
109        i += 1;
110        let c = chars.next().unwrap_or(' ');
111        queue!(stdout, Print(c))?;
112    }
113
114    Ok(())
115}
116
117/// Import this trait to implement `Input::handle_event()` for crossterm.
118pub trait EventHandler {
119    /// Handle crossterm event.
120    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
121}
122
123impl EventHandler for Input {
124    /// Handle crossterm event.
125    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged> {
126        to_input_request(evt).and_then(|req| self.handle(req))
127    }
128}
129
130#[cfg(test)]
131mod tests;