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