1use crossterm::event::{self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEventKind};
6use std::sync::mpsc;
7use std::thread;
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone)]
12pub enum Event {
13 Tick,
15 Key(KeyEvent),
17 Mouse(crossterm::event::MouseEvent),
19 Resize(u16, u16),
21}
22
23pub struct EventHandler {
25 rx: mpsc::Receiver<Event>,
27 _tx: mpsc::Sender<Event>,
29}
30
31impl EventHandler {
32 pub fn new(tick_rate: Duration) -> Self {
34 let (tx, rx) = mpsc::channel();
35 let _tx = tx.clone();
36
37 thread::spawn(move || {
38 let mut last_tick = Instant::now();
39 loop {
40 let timeout = tick_rate
42 .checked_sub(last_tick.elapsed())
43 .unwrap_or(Duration::ZERO);
44
45 if event::poll(timeout).unwrap_or(false) {
47 match event::read() {
48 Ok(CrosstermEvent::Key(key)) => {
49 if key.kind == crossterm::event::KeyEventKind::Press {
51 if tx.send(Event::Key(key)).is_err() {
52 return;
53 }
54 }
55 }
56 Ok(CrosstermEvent::Mouse(mouse)) => {
57 if tx.send(Event::Mouse(mouse)).is_err() {
58 return;
59 }
60 }
61 Ok(CrosstermEvent::Resize(w, h)) => {
62 if tx.send(Event::Resize(w, h)).is_err() {
63 return;
64 }
65 }
66 _ => {}
67 }
68 }
69
70 if last_tick.elapsed() >= tick_rate {
72 if tx.send(Event::Tick).is_err() {
73 return;
74 }
75 last_tick = Instant::now();
76 }
77 }
78 });
79
80 Self { rx, _tx }
81 }
82
83 pub fn next(&self) -> Result<Event, mpsc::RecvError> {
85 self.rx.recv()
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum Action {
92 Quit,
94 Up,
96 Down,
98 ToggleSelect,
100 ToggleExpand,
102 Expand,
104 Collapse,
106 SelectAll,
108 DeselectAll,
110 Delete,
112 Confirm,
114 Cancel,
116 Help,
118 Scan,
120 PageUp,
122 PageDown,
124 Top,
126 Bottom,
128 Search,
130 NextTab,
132 PrevTab,
134 Refresh,
136 ScrollUp,
138 ScrollDown,
140 Back,
142 None,
144}
145
146impl Action {
147 pub fn from_key(key: KeyEvent) -> Self {
149 match key.code {
150 KeyCode::Char('q') => Action::Quit,
152 KeyCode::Esc => Action::Cancel,
153 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::Quit,
154
155 KeyCode::Up | KeyCode::Char('k') => Action::Up,
157 KeyCode::Down | KeyCode::Char('j') => Action::Down,
158 KeyCode::PageUp | KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::PageUp,
159 KeyCode::PageDown | KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::PageDown,
160 KeyCode::Home | KeyCode::Char('g') if key.modifiers.contains(KeyModifiers::NONE) => Action::Top,
161 KeyCode::End | KeyCode::Char('G') => Action::Bottom,
162
163 KeyCode::Right | KeyCode::Char('l') => Action::Expand,
165 KeyCode::Left | KeyCode::Char('h') => Action::Collapse,
166
167 KeyCode::Char(' ') => Action::ToggleSelect,
169 KeyCode::Enter => Action::ToggleExpand,
170 KeyCode::Char('a') => Action::SelectAll,
171 KeyCode::Char('A') => Action::DeselectAll,
172 KeyCode::Char('u') if !key.modifiers.contains(KeyModifiers::CONTROL) => Action::DeselectAll,
173
174 KeyCode::Char('d') if !key.modifiers.contains(KeyModifiers::CONTROL) => Action::Delete,
176 KeyCode::Delete => Action::Delete,
177 KeyCode::Char('y') => Action::Confirm,
178 KeyCode::Char('n') => Action::Cancel,
179 KeyCode::Char('?') => Action::Help,
180 KeyCode::Char('s') => Action::Scan,
181 KeyCode::Char('/') => Action::Search,
182 KeyCode::Char('r') | KeyCode::F(5) => Action::Refresh,
183 KeyCode::Char('b') | KeyCode::Backspace => Action::Back,
184
185 KeyCode::Tab => Action::NextTab,
187 KeyCode::BackTab => Action::PrevTab,
188 KeyCode::Char('1') => Action::None, KeyCode::Char('2') => Action::None,
190
191 _ => Action::None,
192 }
193 }
194
195 pub fn from_mouse(mouse: &crossterm::event::MouseEvent) -> Self {
197 match mouse.kind {
198 MouseEventKind::ScrollUp => Action::ScrollUp,
199 MouseEventKind::ScrollDown => Action::ScrollDown,
200 _ => Action::None,
201 }
202 }
203}