1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
11
12#[derive(Debug, Clone, Copy, Eq, PartialEq)]
17#[allow(missing_docs)]
18pub enum Command {
19 Noop,
20 Quit,
21 Redraw,
22 ScrollDown(u16),
23 ScrollUp(u16),
24 PageDown,
25 PageUp,
26 HalfPageDown,
27 HalfPageUp,
28 Home,
29 End,
30 GotoLine(usize),
32 BeginSearch(SearchDirection),
34 SearchChar(char),
35 SearchBackspace,
36 SearchCommit,
37 SearchCancel,
38 SearchNext,
39 SearchPrev,
40 ClearHighlights,
41 NextHeading,
42 PrevHeading,
43 ToggleToc,
44 TocActivate,
46 SetBookmark(char),
48 JumpBookmark(char),
50 ToggleLineNumbers,
51}
52
53#[derive(Debug, Clone, Copy, Eq, PartialEq)]
55#[allow(missing_docs)]
56pub enum SearchDirection {
57 Forward,
58 Backward,
59}
60
61#[derive(Debug, Default)]
67pub struct Decoder {
68 count: u32,
69 pending_g: bool,
70 pending_bracket: Option<char>,
71 pending_mark_set: bool,
72 pending_mark_jump: bool,
73 searching: bool,
74}
75
76impl Decoder {
77 pub fn in_search(&self) -> bool {
79 self.searching
80 }
81
82 pub fn feed(&mut self, key: KeyEvent) -> Command {
84 let KeyEvent {
85 code, modifiers, ..
86 } = key;
87
88 if modifiers.contains(KeyModifiers::CONTROL) && matches!(code, KeyCode::Char('c')) {
90 *self = Self::default();
91 return Command::Quit;
92 }
93
94 if self.searching {
95 return self.feed_search(code);
96 }
97
98 if self.pending_mark_set {
101 self.pending_mark_set = false;
102 return match code {
103 KeyCode::Char(c) if c.is_ascii_alphabetic() => Command::SetBookmark(c),
104 _ => Command::Noop,
105 };
106 }
107 if self.pending_mark_jump {
108 self.pending_mark_jump = false;
109 return match code {
110 KeyCode::Char(c) if c.is_ascii_alphabetic() => Command::JumpBookmark(c),
111 _ => Command::Noop,
112 };
113 }
114
115 if let KeyCode::Char(c) = code {
118 if c.is_ascii_digit() && modifiers.is_empty() {
119 if self.count == 0 && c == '0' {
122 return Command::Home;
123 }
124 self.count = self.count.saturating_mul(10) + (c as u32 - b'0' as u32);
125 return Command::Noop;
126 }
127 }
128
129 let count = std::mem::take(&mut self.count);
130 let prev_g = std::mem::replace(&mut self.pending_g, false);
131 let prev_bracket = self.pending_bracket.take();
132
133 match (code, modifiers) {
134 (KeyCode::Char('q'), KeyModifiers::NONE) => Command::Quit,
135 (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
136 Command::ScrollDown(count.max(1).try_into().unwrap_or(1))
137 }
138 (KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => {
139 Command::ScrollUp(count.max(1).try_into().unwrap_or(1))
140 }
141 (KeyCode::Char(' '), KeyModifiers::NONE) | (KeyCode::PageDown, _) => Command::PageDown,
142 (KeyCode::Char('f'), KeyModifiers::CONTROL) => Command::PageDown,
143 (KeyCode::Char('b'), KeyModifiers::NONE) | (KeyCode::PageUp, _) => Command::PageUp,
144 (KeyCode::Char('b'), KeyModifiers::CONTROL) => Command::PageUp,
145 (KeyCode::Char('d'), KeyModifiers::CONTROL) => Command::HalfPageDown,
146 (KeyCode::Char('u'), KeyModifiers::CONTROL) => Command::HalfPageUp,
147 (KeyCode::Char('l'), KeyModifiers::CONTROL) => Command::Redraw,
148 (KeyCode::Home, _) => Command::Home,
149 (KeyCode::End, _) => Command::End,
150 (KeyCode::Char('g'), KeyModifiers::NONE) => {
151 if prev_g {
152 Command::Home
153 } else {
154 self.pending_g = true;
155 Command::Noop
156 }
157 }
158 (KeyCode::Char('G'), _) => {
159 if count > 0 {
160 Command::GotoLine(count as usize)
161 } else {
162 Command::End
163 }
164 }
165 (KeyCode::Char('/'), KeyModifiers::NONE) => {
166 self.searching = true;
167 Command::BeginSearch(SearchDirection::Forward)
168 }
169 (KeyCode::Char('?'), _) => {
170 self.searching = true;
171 Command::BeginSearch(SearchDirection::Backward)
172 }
173 (KeyCode::Char('n'), KeyModifiers::NONE) => Command::SearchNext,
174 (KeyCode::Char('N'), _) => Command::SearchPrev,
175 (KeyCode::Char(']'), KeyModifiers::NONE) => {
176 if prev_bracket == Some(']') {
177 Command::NextHeading
178 } else {
179 self.pending_bracket = Some(']');
180 Command::Noop
181 }
182 }
183 (KeyCode::Char('['), KeyModifiers::NONE) => {
184 if prev_bracket == Some('[') {
185 Command::PrevHeading
186 } else {
187 self.pending_bracket = Some('[');
188 Command::Noop
189 }
190 }
191 (KeyCode::Char('T'), _) => Command::ToggleToc,
192 (KeyCode::Char('m'), KeyModifiers::NONE) => {
193 self.pending_mark_set = true;
194 Command::Noop
195 }
196 (KeyCode::Char('\''), KeyModifiers::NONE) => {
197 self.pending_mark_jump = true;
198 Command::Noop
199 }
200 (KeyCode::Enter, _) => Command::TocActivate,
201 (KeyCode::Char('#'), _) => Command::ToggleLineNumbers,
202 (KeyCode::Esc, _) => Command::ClearHighlights,
203 _ => Command::Noop,
204 }
205 }
206
207 fn feed_search(&mut self, code: KeyCode) -> Command {
209 match code {
210 KeyCode::Enter => {
211 self.searching = false;
212 Command::SearchCommit
213 }
214 KeyCode::Esc => {
215 self.searching = false;
216 Command::SearchCancel
217 }
218 KeyCode::Backspace => Command::SearchBackspace,
219 KeyCode::Char(c) => Command::SearchChar(c),
220 _ => Command::Noop,
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 fn key(c: char) -> KeyEvent {
230 KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
231 }
232
233 fn key_mod(c: char, m: KeyModifiers) -> KeyEvent {
234 KeyEvent::new(KeyCode::Char(c), m)
235 }
236
237 #[test]
238 fn single_keys_map_directly() {
239 let mut d = Decoder::default();
240 assert_eq!(d.feed(key('j')), Command::ScrollDown(1));
241 assert_eq!(d.feed(key('k')), Command::ScrollUp(1));
242 assert_eq!(d.feed(key(' ')), Command::PageDown);
243 assert_eq!(d.feed(key('b')), Command::PageUp);
244 assert_eq!(d.feed(key('G')), Command::End);
245 assert_eq!(d.feed(key('q')), Command::Quit);
246 }
247
248 #[test]
249 fn double_g_is_home() {
250 let mut d = Decoder::default();
251 assert_eq!(d.feed(key('g')), Command::Noop);
252 assert_eq!(d.feed(key('g')), Command::Home);
253 }
254
255 #[test]
256 fn numeric_prefix_drives_goto_line() {
257 let mut d = Decoder::default();
258 for c in "42".chars() {
259 assert_eq!(d.feed(key(c)), Command::Noop);
260 }
261 assert_eq!(d.feed(key('G')), Command::GotoLine(42));
262 }
263
264 #[test]
265 fn numeric_prefix_multiplies_scroll() {
266 let mut d = Decoder::default();
267 assert_eq!(d.feed(key('5')), Command::Noop);
268 assert_eq!(d.feed(key('j')), Command::ScrollDown(5));
269 }
270
271 #[test]
272 fn ctrl_c_quits_mid_prefix() {
273 let mut d = Decoder::default();
274 assert_eq!(d.feed(key('9')), Command::Noop);
275 assert_eq!(d.feed(key_mod('c', KeyModifiers::CONTROL)), Command::Quit);
276 assert_eq!(d.feed(key('G')), Command::End);
278 }
279
280 #[test]
281 fn lone_zero_goes_to_first_column() {
282 let mut d = Decoder::default();
283 assert_eq!(d.feed(key('0')), Command::Home);
284 }
285
286 #[test]
287 fn unknown_key_is_noop_not_error() {
288 let mut d = Decoder::default();
289 assert_eq!(d.feed(key('x')), Command::Noop);
290 }
291
292 #[test]
293 fn double_bracket_emits_heading_jumps() {
294 let mut d = Decoder::default();
295 assert_eq!(d.feed(key(']')), Command::Noop);
296 assert_eq!(d.feed(key(']')), Command::NextHeading);
297 assert_eq!(d.feed(key('[')), Command::Noop);
298 assert_eq!(d.feed(key('[')), Command::PrevHeading);
299 }
300
301 #[test]
302 fn mismatched_bracket_cancels_pending() {
303 let mut d = Decoder::default();
304 assert_eq!(d.feed(key(']')), Command::Noop);
305 assert_eq!(d.feed(key('j')), Command::ScrollDown(1));
307 assert_eq!(d.feed(key(']')), Command::Noop);
309 }
310
311 #[test]
312 fn capital_t_toggles_toc() {
313 let mut d = Decoder::default();
314 assert_eq!(d.feed(key('T')), Command::ToggleToc);
315 }
316
317 #[test]
318 fn m_letter_sets_bookmark_register() {
319 let mut d = Decoder::default();
320 assert_eq!(d.feed(key('m')), Command::Noop);
321 assert_eq!(d.feed(key('a')), Command::SetBookmark('a'));
322 }
323
324 #[test]
325 fn apostrophe_letter_jumps_to_bookmark() {
326 let mut d = Decoder::default();
327 assert_eq!(d.feed(key('\'')), Command::Noop);
328 assert_eq!(d.feed(key('q')), Command::JumpBookmark('q'));
329 }
330
331 #[test]
332 fn hash_toggles_line_numbers() {
333 let mut d = Decoder::default();
334 assert_eq!(d.feed(key('#')), Command::ToggleLineNumbers);
335 }
336
337 #[test]
338 fn bookmark_register_rejects_non_letter() {
339 let mut d = Decoder::default();
340 assert_eq!(d.feed(key('m')), Command::Noop);
341 assert_eq!(d.feed(key('1')), Command::Noop);
342 assert_eq!(d.feed(key('j')), Command::ScrollDown(1));
344 }
345}