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 Noop,
75}
76
77pub fn translate(event: Event) -> Command {
78 match event {
79 Event::Resize(c, r) => Command::Resize(c, r),
80 Event::Key(KeyEvent { code, modifiers, .. }) => translate_key(code, modifiers),
81 _ => Command::Noop,
82 }
83}
84
85fn translate_key(code: KeyCode, mods: KeyModifiers) -> Command {
86 use KeyCode::*;
87 let ctrl = mods.contains(KeyModifiers::CONTROL);
88 match (code, ctrl) {
89 (Char('q'), false) | (Char('Q'), false) => Command::Quit,
90 (Char('c'), true) => Command::Quit,
91 (Down, _) | (Char('j'), false) | (Char('e'), false) | (Char('e'), true) | (Enter, _) => Command::ScrollLines(1),
92 (Char('y'), false) | (Char('y'), true) | (Up, _) | (Char('k'), false) => Command::ScrollLines(-1),
93 (Char('J'), false) => Command::ScrollLogicalLines(1),
94 (Char('K'), false) => Command::ScrollLogicalLines(-1),
95 (Char(' '), false) | (Char('f'), false) | (Char('f'), true) | (PageDown, _) => Command::PageDown,
96 (Char('b'), false) | (Char('b'), true) | (PageUp, _) => Command::PageUp,
97 (Char('d'), false) | (Char('d'), true) => Command::HalfPageDown,
98 (Char('u'), false) | (Char('u'), true) => Command::HalfPageUp,
99 (Char('0'), false) => Command::Digit(0),
100 (Char('1'), false) => Command::Digit(1),
101 (Char('2'), false) => Command::Digit(2),
102 (Char('3'), false) => Command::Digit(3),
103 (Char('4'), false) => Command::Digit(4),
104 (Char('5'), false) => Command::Digit(5),
105 (Char('6'), false) => Command::Digit(6),
106 (Char('7'), false) => Command::Digit(7),
107 (Char('8'), false) => Command::Digit(8),
108 (Char('9'), false) => Command::Digit(9),
109 (Char('g'), false) | (Char('<'), false) | (Home, _) => Command::GotoLine,
110 (Char('G'), false) | (Char('>'), false) | (End, _) => Command::GotoRecord,
111 (Char('%'), false) => Command::GotoPercent,
112 (Esc, _) => Command::Cancel,
113 (Char('r'), false) | (Char('l'), true) => Command::Refresh,
114 (Char('R'), false) => Command::Reload,
115 (Char('P'), false) => Command::TogglePrettify,
116 (Char('-'), false) => Command::OptionPrefix,
117 (Char('F'), false) => Command::ToggleFollow,
118 (Char('/'), false) => Command::SearchForward,
119 (Char('?'), false) => Command::SearchBackward,
120 (Char('n'), false) => Command::NextMatch,
121 (Char('N'), false) => Command::PreviousMatch,
122 (Char('m'), false) => Command::MarkSet,
123 (Char('\''), false) => Command::MarkJump,
124 (Char('!'), false) => Command::ShellEscape,
125 (Char('x'), true) => Command::CtrlXPrefix,
126 (Char(':'), false) => Command::ColonPrompt,
127 _ => Command::Noop,
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crossterm::event::{KeyCode, KeyEventKind, KeyEventState};
135
136 fn key(code: KeyCode, mods: KeyModifiers) -> Event {
137 Event::Key(KeyEvent {
138 code, modifiers: mods,
139 kind: KeyEventKind::Press, state: KeyEventState::NONE,
140 })
141 }
142
143 #[test]
144 fn arrow_down_scrolls_one() {
145 assert_eq!(translate(key(KeyCode::Down, KeyModifiers::NONE)), Command::ScrollLines(1));
146 }
147
148 #[test]
149 fn j_scrolls_one() {
150 assert_eq!(translate(key(KeyCode::Char('j'), KeyModifiers::NONE)), Command::ScrollLines(1));
151 }
152
153 #[test]
154 fn space_pages_down() {
155 assert_eq!(translate(key(KeyCode::Char(' '), KeyModifiers::NONE)), Command::PageDown);
156 }
157
158 #[test]
159 fn ctrl_c_quits() {
160 assert_eq!(translate(key(KeyCode::Char('c'), KeyModifiers::CONTROL)), Command::Quit);
161 }
162
163 #[test]
164 fn capital_g_goes_to_record() {
165 assert_eq!(translate(key(KeyCode::Char('G'), KeyModifiers::SHIFT)), Command::GotoRecord);
166 }
167
168 #[test]
169 fn lowercase_g_goes_to_line() {
170 assert_eq!(translate(key(KeyCode::Char('g'), KeyModifiers::NONE)), Command::GotoLine);
171 }
172
173 #[test]
174 fn percent_goes_to_percent() {
175 assert_eq!(translate(key(KeyCode::Char('%'), KeyModifiers::NONE)), Command::GotoPercent);
176 }
177
178 #[test]
179 fn digit_keys_produce_digit_commands() {
180 for d in 0u8..=9 {
181 let ch = char::from_digit(d as u32, 10).unwrap();
182 assert_eq!(
183 translate(key(KeyCode::Char(ch), KeyModifiers::NONE)),
184 Command::Digit(d),
185 );
186 }
187 }
188
189 #[test]
190 fn esc_produces_cancel() {
191 assert_eq!(translate(key(KeyCode::Esc, KeyModifiers::NONE)), Command::Cancel);
192 }
193
194 #[test]
195 fn capital_j_jumps_one_logical_line_forward() {
196 assert_eq!(translate(key(KeyCode::Char('J'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(1));
197 }
198
199 #[test]
200 fn capital_k_jumps_one_logical_line_backward() {
201 assert_eq!(translate(key(KeyCode::Char('K'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(-1));
202 }
203
204 #[test]
205 fn capital_f_toggles_follow() {
206 assert_eq!(translate(key(KeyCode::Char('F'), KeyModifiers::SHIFT)), Command::ToggleFollow);
207 }
208
209 #[test]
210 fn lowercase_f_still_pages_down() {
211 assert_eq!(translate(key(KeyCode::Char('f'), KeyModifiers::NONE)), Command::PageDown);
212 }
213
214 #[test]
215 fn slash_opens_forward_search() {
216 assert_eq!(translate(key(KeyCode::Char('/'), KeyModifiers::NONE)), Command::SearchForward);
217 }
218
219 #[test]
220 fn question_mark_opens_backward_search() {
221 assert_eq!(translate(key(KeyCode::Char('?'), KeyModifiers::SHIFT)), Command::SearchBackward);
223 }
224
225 #[test]
226 fn n_repeats_match_forward() {
227 assert_eq!(translate(key(KeyCode::Char('n'), KeyModifiers::NONE)), Command::NextMatch);
228 }
229
230 #[test]
231 fn capital_n_repeats_match_backward() {
232 assert_eq!(translate(key(KeyCode::Char('N'), KeyModifiers::SHIFT)), Command::PreviousMatch);
233 }
234
235 #[test]
236 fn capital_r_triggers_reload() {
237 assert_eq!(translate(key(KeyCode::Char('R'), KeyModifiers::SHIFT)), Command::Reload);
238 }
239
240 #[test]
241 fn lowercase_r_still_refreshes() {
242 assert_eq!(translate(key(KeyCode::Char('r'), KeyModifiers::NONE)), Command::Refresh);
243 }
244
245 #[test]
246 fn capital_p_toggles_prettify() {
247 assert_eq!(translate(key(KeyCode::Char('P'), KeyModifiers::SHIFT)), Command::TogglePrettify);
248 }
249
250 #[test]
251 fn lowercase_p_remains_unbound() {
252 assert_eq!(translate(key(KeyCode::Char('p'), KeyModifiers::NONE)), Command::Noop);
253 }
254
255 #[test]
256 fn dash_is_option_prefix() {
257 assert_eq!(translate(key(KeyCode::Char('-'), KeyModifiers::NONE)), Command::OptionPrefix);
258 }
259
260 #[test]
261 fn resize_event() {
262 assert_eq!(translate(Event::Resize(80, 24)), Command::Resize(80, 24));
263 }
264
265 #[test]
266 fn m_key_produces_mark_set_command() {
267 let evt = key(KeyCode::Char('m'), KeyModifiers::NONE);
268 assert_eq!(translate(evt), Command::MarkSet);
269 }
270
271 #[test]
272 fn single_quote_key_produces_mark_jump_command() {
273 let evt = key(KeyCode::Char('\''), KeyModifiers::NONE);
274 assert_eq!(translate(evt), Command::MarkJump);
275 }
276
277 #[test]
278 fn ctrl_x_produces_ctrl_x_prefix_command() {
279 let evt = key(KeyCode::Char('x'), KeyModifiers::CONTROL);
280 assert_eq!(translate(evt), Command::CtrlXPrefix);
281 }
282
283 #[test]
284 fn bang_produces_shell_escape_command() {
285 let evt = key(KeyCode::Char('!'), KeyModifiers::NONE);
286 assert_eq!(translate(evt), Command::ShellEscape);
287 }
288
289 #[test]
290 fn colon_produces_colon_prompt_command() {
291 let evt = Event::Key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE));
292 assert_eq!(translate(evt), Command::ColonPrompt);
293 }
294}