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 TogglePermanent,
144 None,
146}
147
148impl Action {
149 pub fn from_key(key: KeyEvent) -> Self {
151 match key.code {
152 KeyCode::Char('q') => Action::Quit,
154 KeyCode::Esc => Action::Cancel,
155 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::Quit,
156
157 KeyCode::Up | KeyCode::Char('k') => Action::Up,
159 KeyCode::Down | KeyCode::Char('j') => Action::Down,
160 KeyCode::PageUp | KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::PageUp,
161 KeyCode::PageDown | KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::PageDown,
162 KeyCode::Home | KeyCode::Char('g') if key.modifiers.contains(KeyModifiers::NONE) => Action::Top,
163 KeyCode::End | KeyCode::Char('G') => Action::Bottom,
164
165 KeyCode::Right | KeyCode::Char('l') => Action::Expand,
167 KeyCode::Left | KeyCode::Char('h') => Action::Collapse,
168
169 KeyCode::Enter => Action::ToggleSelect,
171 KeyCode::Char(' ') => Action::ToggleExpand,
172 KeyCode::Char('a') => Action::SelectAll,
173 KeyCode::Char('A') => Action::DeselectAll,
174 KeyCode::Char('u') if !key.modifiers.contains(KeyModifiers::CONTROL) => Action::DeselectAll,
175
176 KeyCode::Char('d') if !key.modifiers.contains(KeyModifiers::CONTROL) => Action::Delete,
178 KeyCode::Delete => Action::Delete,
179 KeyCode::Char('y') => Action::Confirm,
180 KeyCode::Char('n') => Action::Cancel,
181 KeyCode::Char('p') => Action::TogglePermanent,
182 KeyCode::Char('?') => Action::Help,
183 KeyCode::Char('s') => Action::Scan,
184 KeyCode::Char('/') => Action::Search,
185 KeyCode::Char('r') | KeyCode::F(5) => Action::Refresh,
186 KeyCode::Char('b') | KeyCode::Backspace => Action::Back,
187
188 KeyCode::Tab => Action::NextTab,
190 KeyCode::BackTab => Action::PrevTab,
191 KeyCode::Char('1') => Action::None, KeyCode::Char('2') => Action::None,
193
194 _ => Action::None,
195 }
196 }
197
198 pub fn from_mouse(mouse: &crossterm::event::MouseEvent) -> Self {
200 match mouse.kind {
201 MouseEventKind::ScrollUp => Action::ScrollUp,
202 MouseEventKind::ScrollDown => Action::ScrollDown,
203 _ => Action::None,
204 }
205 }
206}