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 (Char(c), modifiers) if modifiers == KeyModifiers::CONTROL | KeyModifiers::ALT => Some(InsertChar(c)),
63 (_, _) => None,
64 }
65 }
66 _ => None,
67 }
68}
69
70pub 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 while i < cursor {
88 i += 1;
89 let c = chars.next().unwrap_or(' ');
90 queue!(stdout, Print(c))?;
91 }
92
93 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 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
113pub trait EventHandler {
115 fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
117}
118
119impl EventHandler for Input {
120 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}