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