tui_input/backend/
crossterm.rs1use 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
12pub 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
65pub 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 while i < cursor {
83 i += 1;
84 let c = chars.next().unwrap_or(' ');
85 queue!(stdout, Print(c))?;
86 }
87
88 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 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
108pub trait EventHandler {
110 fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
112}
113
114impl EventHandler for Input {
115 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}