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
15pub 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
68pub 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 while i < cursor {
86 i += 1;
87 let c = chars.next().unwrap_or(' ');
88 queue!(stdout, Print(c))?;
89 }
90
91 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 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
111pub trait EventHandler {
113 fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
115}
116
117impl EventHandler for Input {
118 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}