1use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
2
3use crate::prettify::PrettifyMode;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum Command {
7 ScrollLines(i64),
8 ScrollLogicalLines(i64),
12 PageDown,
13 PageUp,
14 HalfPageDown,
15 HalfPageUp,
16 Quit,
17 Resize(u16, u16),
18 Refresh,
19 ToggleLineNumbers,
20 ToggleChop,
21 ToggleFollow,
22 SearchForward,
24 SearchBackward,
26 NextMatch,
28 PreviousMatch,
30 OptionPrefix,
33 Reload,
36 TogglePrettify,
39 SetPrettifyMode(PrettifyMode),
42 RedetectPrettify,
44 Digit(u8),
47 GotoLine,
50 GotoRecord,
53 GotoPercent,
56 Cancel,
58 MarkSet,
61 MarkJump,
64 CtrlXPrefix,
67 JumpPrevious,
70 ShellEscape,
72 ColonPrompt,
74 TagPrompt,
76 TagPop,
78 OpenPicker,
80 OpenTagPicker,
83 OpenHelp,
85 SelectFile(usize),
88 DropFileAt(usize),
91 SelectTagMatch(usize),
94 MouseEvent(crossterm::event::MouseEvent),
98 Noop,
99}
100
101pub fn translate(event: Event) -> Command {
102 match event {
103 Event::Resize(c, r) => Command::Resize(c, r),
104 Event::Key(KeyEvent { code, modifiers, .. }) => translate_key(code, modifiers),
105 Event::Mouse(m) => Command::MouseEvent(m),
106 _ => Command::Noop,
107 }
108}
109
110fn translate_key(code: KeyCode, mods: KeyModifiers) -> Command {
111 use KeyCode::*;
112 let ctrl = mods.contains(KeyModifiers::CONTROL);
113 match (code, ctrl) {
114 (Char('q'), false) | (Char('Q'), false) => Command::Quit,
115 (Char('c'), true) => Command::Quit,
116 (Down, _) | (Char('j'), false) | (Char('e'), false) | (Char('e'), true) | (Enter, _) => Command::ScrollLines(1),
117 (Char('y'), false) | (Char('y'), true) | (Up, _) | (Char('k'), false) => Command::ScrollLines(-1),
118 (Char('J'), false) => Command::ScrollLogicalLines(1),
119 (Char('K'), false) => Command::ScrollLogicalLines(-1),
120 (Char(' '), false) | (Char('f'), false) | (Char('f'), true) | (PageDown, _) => Command::PageDown,
121 (Char('b'), false) | (Char('b'), true) | (PageUp, _) => Command::PageUp,
122 (Char('d'), false) | (Char('d'), true) => Command::HalfPageDown,
123 (Char('u'), false) | (Char('u'), true) => Command::HalfPageUp,
124 (Char('0'), false) => Command::Digit(0),
125 (Char('1'), false) => Command::Digit(1),
126 (Char('2'), false) => Command::Digit(2),
127 (Char('3'), false) => Command::Digit(3),
128 (Char('4'), false) => Command::Digit(4),
129 (Char('5'), false) => Command::Digit(5),
130 (Char('6'), false) => Command::Digit(6),
131 (Char('7'), false) => Command::Digit(7),
132 (Char('8'), false) => Command::Digit(8),
133 (Char('9'), false) => Command::Digit(9),
134 (Char('g'), false) | (Char('<'), false) | (Home, _) => Command::GotoLine,
135 (Char('G'), false) | (Char('>'), false) | (End, _) => Command::GotoRecord,
136 (Char('%'), false) => Command::GotoPercent,
137 (Esc, _) => Command::Cancel,
138 (Char('r'), false) | (Char('l'), true) => Command::Refresh,
139 (Char('R'), false) => Command::Reload,
140 (Char('P'), false) => Command::TogglePrettify,
141 (Char('-'), false) => Command::OptionPrefix,
142 (Char('F'), false) => Command::ToggleFollow,
143 (Char('/'), false) => Command::SearchForward,
144 (Char('?'), false) => Command::SearchBackward,
145 (Char('n'), false) => Command::NextMatch,
146 (Char('N'), false) => Command::PreviousMatch,
147 (Char('m'), false) => Command::MarkSet,
148 (Char('\''), false) => Command::MarkJump,
149 (Char('!'), false) => Command::ShellEscape,
150 (Char('x'), true) => Command::CtrlXPrefix,
151 (Char(':'), false) => Command::ColonPrompt,
152 (Char(']'), true) => Command::TagPrompt,
153 (Char('t'), true) => Command::TagPop,
154 (F(1), _) => Command::OpenHelp,
155 _ => Command::Noop,
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crossterm::event::{KeyCode, KeyEventKind, KeyEventState};
163
164 fn key(code: KeyCode, mods: KeyModifiers) -> Event {
165 Event::Key(KeyEvent {
166 code, modifiers: mods,
167 kind: KeyEventKind::Press, state: KeyEventState::NONE,
168 })
169 }
170
171 #[test]
172 fn arrow_down_scrolls_one() {
173 assert_eq!(translate(key(KeyCode::Down, KeyModifiers::NONE)), Command::ScrollLines(1));
174 }
175
176 #[test]
177 fn j_scrolls_one() {
178 assert_eq!(translate(key(KeyCode::Char('j'), KeyModifiers::NONE)), Command::ScrollLines(1));
179 }
180
181 #[test]
182 fn space_pages_down() {
183 assert_eq!(translate(key(KeyCode::Char(' '), KeyModifiers::NONE)), Command::PageDown);
184 }
185
186 #[test]
187 fn ctrl_c_quits() {
188 assert_eq!(translate(key(KeyCode::Char('c'), KeyModifiers::CONTROL)), Command::Quit);
189 }
190
191 #[test]
192 fn capital_g_goes_to_record() {
193 assert_eq!(translate(key(KeyCode::Char('G'), KeyModifiers::SHIFT)), Command::GotoRecord);
194 }
195
196 #[test]
197 fn lowercase_g_goes_to_line() {
198 assert_eq!(translate(key(KeyCode::Char('g'), KeyModifiers::NONE)), Command::GotoLine);
199 }
200
201 #[test]
202 fn percent_goes_to_percent() {
203 assert_eq!(translate(key(KeyCode::Char('%'), KeyModifiers::NONE)), Command::GotoPercent);
204 }
205
206 #[test]
207 fn digit_keys_produce_digit_commands() {
208 for d in 0u8..=9 {
209 let ch = char::from_digit(d as u32, 10).unwrap();
210 assert_eq!(
211 translate(key(KeyCode::Char(ch), KeyModifiers::NONE)),
212 Command::Digit(d),
213 );
214 }
215 }
216
217 #[test]
218 fn esc_produces_cancel() {
219 assert_eq!(translate(key(KeyCode::Esc, KeyModifiers::NONE)), Command::Cancel);
220 }
221
222 #[test]
223 fn capital_j_jumps_one_logical_line_forward() {
224 assert_eq!(translate(key(KeyCode::Char('J'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(1));
225 }
226
227 #[test]
228 fn capital_k_jumps_one_logical_line_backward() {
229 assert_eq!(translate(key(KeyCode::Char('K'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(-1));
230 }
231
232 #[test]
233 fn capital_f_toggles_follow() {
234 assert_eq!(translate(key(KeyCode::Char('F'), KeyModifiers::SHIFT)), Command::ToggleFollow);
235 }
236
237 #[test]
238 fn lowercase_f_still_pages_down() {
239 assert_eq!(translate(key(KeyCode::Char('f'), KeyModifiers::NONE)), Command::PageDown);
240 }
241
242 #[test]
243 fn slash_opens_forward_search() {
244 assert_eq!(translate(key(KeyCode::Char('/'), KeyModifiers::NONE)), Command::SearchForward);
245 }
246
247 #[test]
248 fn question_mark_opens_backward_search() {
249 assert_eq!(translate(key(KeyCode::Char('?'), KeyModifiers::SHIFT)), Command::SearchBackward);
251 }
252
253 #[test]
254 fn n_repeats_match_forward() {
255 assert_eq!(translate(key(KeyCode::Char('n'), KeyModifiers::NONE)), Command::NextMatch);
256 }
257
258 #[test]
259 fn capital_n_repeats_match_backward() {
260 assert_eq!(translate(key(KeyCode::Char('N'), KeyModifiers::SHIFT)), Command::PreviousMatch);
261 }
262
263 #[test]
264 fn capital_r_triggers_reload() {
265 assert_eq!(translate(key(KeyCode::Char('R'), KeyModifiers::SHIFT)), Command::Reload);
266 }
267
268 #[test]
269 fn lowercase_r_still_refreshes() {
270 assert_eq!(translate(key(KeyCode::Char('r'), KeyModifiers::NONE)), Command::Refresh);
271 }
272
273 #[test]
274 fn capital_p_toggles_prettify() {
275 assert_eq!(translate(key(KeyCode::Char('P'), KeyModifiers::SHIFT)), Command::TogglePrettify);
276 }
277
278 #[test]
279 fn lowercase_p_remains_unbound() {
280 assert_eq!(translate(key(KeyCode::Char('p'), KeyModifiers::NONE)), Command::Noop);
281 }
282
283 #[test]
284 fn dash_is_option_prefix() {
285 assert_eq!(translate(key(KeyCode::Char('-'), KeyModifiers::NONE)), Command::OptionPrefix);
286 }
287
288 #[test]
289 fn resize_event() {
290 assert_eq!(translate(Event::Resize(80, 24)), Command::Resize(80, 24));
291 }
292
293 #[test]
294 fn m_key_produces_mark_set_command() {
295 let evt = key(KeyCode::Char('m'), KeyModifiers::NONE);
296 assert_eq!(translate(evt), Command::MarkSet);
297 }
298
299 #[test]
300 fn single_quote_key_produces_mark_jump_command() {
301 let evt = key(KeyCode::Char('\''), KeyModifiers::NONE);
302 assert_eq!(translate(evt), Command::MarkJump);
303 }
304
305 #[test]
306 fn ctrl_x_produces_ctrl_x_prefix_command() {
307 let evt = key(KeyCode::Char('x'), KeyModifiers::CONTROL);
308 assert_eq!(translate(evt), Command::CtrlXPrefix);
309 }
310
311 #[test]
312 fn bang_produces_shell_escape_command() {
313 let evt = key(KeyCode::Char('!'), KeyModifiers::NONE);
314 assert_eq!(translate(evt), Command::ShellEscape);
315 }
316
317 #[test]
318 fn colon_produces_colon_prompt_command() {
319 let evt = Event::Key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE));
320 assert_eq!(translate(evt), Command::ColonPrompt);
321 }
322
323 #[test]
324 fn ctrl_close_bracket_produces_tag_prompt() {
325 let evt = Event::Key(KeyEvent::new(KeyCode::Char(']'), KeyModifiers::CONTROL));
326 assert_eq!(translate(evt), Command::TagPrompt);
327 }
328
329 #[test]
330 fn ctrl_t_produces_tag_pop() {
331 let evt = Event::Key(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::CONTROL));
332 assert_eq!(translate(evt), Command::TagPop);
333 }
334
335 #[test]
336 fn f1_opens_help() {
337 let evt = key(KeyCode::F(1), KeyModifiers::NONE);
338 assert_eq!(translate(evt), Command::OpenHelp);
339 }
340}