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