Skip to main content

tess/
input.rs

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    /// `J` / `K` — jump forward or backward by one whole logical line,
9    /// skipping any remaining wrap rows of the current line. Useful for
10    /// long lines that wrap many screen rows.
11    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    /// `/` — open the forward-search prompt.
23    SearchForward,
24    /// `?` — open the backward-search prompt.
25    SearchBackward,
26    /// `n` — repeat the last search in its original direction.
27    NextMatch,
28    /// `N` — repeat the last search in the opposite direction.
29    PreviousMatch,
30    /// `-` — option-toggle prefix: the next key chooses an option to flip
31    /// (`N` → line numbers, `S` → chop, `F` → follow).
32    OptionPrefix,
33    /// `R` — force-reload the source from disk now (only meaningful with
34    /// `--live`; no-op for static file sources and append-streaming follow).
35    Reload,
36    /// `Shift-P` — toggle pretty-printing on/off (cycles back to the last
37    /// active mode if currently off).
38    TogglePrettify,
39    /// Set a specific prettify mode (issued by the `-P<letter>` sub-prefix
40    /// after the user picks j/y/t/x/h/c).
41    SetPrettifyMode(PrettifyMode),
42    /// Re-run byte-based content detection and apply the result (`-Pa`).
43    RedetectPrettify,
44    /// A digit (0-9) was pressed. The app accumulates these into a numeric
45    /// prefix that the next non-digit command consumes.
46    Digit(u8),
47    /// Jump to physical line N (1-indexed). Without a prefix, behaves as
48    /// goto-top.
49    GotoLine,
50    /// Jump to record N (1-indexed). Without a prefix, behaves as
51    /// goto-bottom (preserves the existing bare-`G` behavior).
52    GotoRecord,
53    /// Jump to N percent through the file by bytes. Without a prefix,
54    /// behaves as goto-top.
55    GotoPercent,
56    /// Cancel any pending numeric prefix without firing a command.
57    Cancel,
58    /// First half of a set-mark sequence (the `m` key). The next keystroke
59    /// names the mark.
60    MarkSet,
61    /// First half of a jump-to-mark sequence (the `'` key). The next
62    /// keystroke names the mark.
63    MarkJump,
64    /// First half of the `Ctrl-X Ctrl-X` jump-to-previous-position chord.
65    /// The next keystroke must also be Ctrl-X.
66    CtrlXPrefix,
67    /// Jump to the previous position (Ctrl-X Ctrl-X in less). Dispatched
68    /// from the CtrlXPending mode intercept in app.rs.
69    JumpPrevious,
70    Noop,
71}
72
73pub fn translate(event: Event) -> Command {
74    match event {
75        Event::Resize(c, r) => Command::Resize(c, r),
76        Event::Key(KeyEvent { code, modifiers, .. }) => translate_key(code, modifiers),
77        _ => Command::Noop,
78    }
79}
80
81fn translate_key(code: KeyCode, mods: KeyModifiers) -> Command {
82    use KeyCode::*;
83    let ctrl = mods.contains(KeyModifiers::CONTROL);
84    match (code, ctrl) {
85        (Char('q'), false) | (Char('Q'), false) => Command::Quit,
86        (Char('c'), true) => Command::Quit,
87        (Down, _) | (Char('j'), false) | (Char('e'), false) | (Char('e'), true) | (Enter, _) => Command::ScrollLines(1),
88        (Char('y'), false) | (Char('y'), true) | (Up, _) | (Char('k'), false) => Command::ScrollLines(-1),
89        (Char('J'), false) => Command::ScrollLogicalLines(1),
90        (Char('K'), false) => Command::ScrollLogicalLines(-1),
91        (Char(' '), false) | (Char('f'), false) | (Char('f'), true) | (PageDown, _) => Command::PageDown,
92        (Char('b'), false) | (Char('b'), true) | (PageUp, _) => Command::PageUp,
93        (Char('d'), false) | (Char('d'), true) => Command::HalfPageDown,
94        (Char('u'), false) | (Char('u'), true) => Command::HalfPageUp,
95        (Char('0'), false) => Command::Digit(0),
96        (Char('1'), false) => Command::Digit(1),
97        (Char('2'), false) => Command::Digit(2),
98        (Char('3'), false) => Command::Digit(3),
99        (Char('4'), false) => Command::Digit(4),
100        (Char('5'), false) => Command::Digit(5),
101        (Char('6'), false) => Command::Digit(6),
102        (Char('7'), false) => Command::Digit(7),
103        (Char('8'), false) => Command::Digit(8),
104        (Char('9'), false) => Command::Digit(9),
105        (Char('g'), false) | (Char('<'), false) | (Home, _) => Command::GotoLine,
106        (Char('G'), false) | (Char('>'), false) | (End, _) => Command::GotoRecord,
107        (Char('%'), false) => Command::GotoPercent,
108        (Esc, _) => Command::Cancel,
109        (Char('r'), false) | (Char('l'), true) => Command::Refresh,
110        (Char('R'), false) => Command::Reload,
111        (Char('P'), false) => Command::TogglePrettify,
112        (Char('-'), false) => Command::OptionPrefix,
113        (Char('F'), false) => Command::ToggleFollow,
114        (Char('/'), false) => Command::SearchForward,
115        (Char('?'), false) => Command::SearchBackward,
116        (Char('n'), false) => Command::NextMatch,
117        (Char('N'), false) => Command::PreviousMatch,
118        (Char('m'), false) => Command::MarkSet,
119        (Char('\''), false) => Command::MarkJump,
120        (Char('x'), true) => Command::CtrlXPrefix,
121        _ => Command::Noop,
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crossterm::event::{KeyCode, KeyEventKind, KeyEventState};
129
130    fn key(code: KeyCode, mods: KeyModifiers) -> Event {
131        Event::Key(KeyEvent {
132            code, modifiers: mods,
133            kind: KeyEventKind::Press, state: KeyEventState::NONE,
134        })
135    }
136
137    #[test]
138    fn arrow_down_scrolls_one() {
139        assert_eq!(translate(key(KeyCode::Down, KeyModifiers::NONE)), Command::ScrollLines(1));
140    }
141
142    #[test]
143    fn j_scrolls_one() {
144        assert_eq!(translate(key(KeyCode::Char('j'), KeyModifiers::NONE)), Command::ScrollLines(1));
145    }
146
147    #[test]
148    fn space_pages_down() {
149        assert_eq!(translate(key(KeyCode::Char(' '), KeyModifiers::NONE)), Command::PageDown);
150    }
151
152    #[test]
153    fn ctrl_c_quits() {
154        assert_eq!(translate(key(KeyCode::Char('c'), KeyModifiers::CONTROL)), Command::Quit);
155    }
156
157    #[test]
158    fn capital_g_goes_to_record() {
159        assert_eq!(translate(key(KeyCode::Char('G'), KeyModifiers::SHIFT)), Command::GotoRecord);
160    }
161
162    #[test]
163    fn lowercase_g_goes_to_line() {
164        assert_eq!(translate(key(KeyCode::Char('g'), KeyModifiers::NONE)), Command::GotoLine);
165    }
166
167    #[test]
168    fn percent_goes_to_percent() {
169        assert_eq!(translate(key(KeyCode::Char('%'), KeyModifiers::NONE)), Command::GotoPercent);
170    }
171
172    #[test]
173    fn digit_keys_produce_digit_commands() {
174        for d in 0u8..=9 {
175            let ch = char::from_digit(d as u32, 10).unwrap();
176            assert_eq!(
177                translate(key(KeyCode::Char(ch), KeyModifiers::NONE)),
178                Command::Digit(d),
179            );
180        }
181    }
182
183    #[test]
184    fn esc_produces_cancel() {
185        assert_eq!(translate(key(KeyCode::Esc, KeyModifiers::NONE)), Command::Cancel);
186    }
187
188    #[test]
189    fn capital_j_jumps_one_logical_line_forward() {
190        assert_eq!(translate(key(KeyCode::Char('J'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(1));
191    }
192
193    #[test]
194    fn capital_k_jumps_one_logical_line_backward() {
195        assert_eq!(translate(key(KeyCode::Char('K'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(-1));
196    }
197
198    #[test]
199    fn capital_f_toggles_follow() {
200        assert_eq!(translate(key(KeyCode::Char('F'), KeyModifiers::SHIFT)), Command::ToggleFollow);
201    }
202
203    #[test]
204    fn lowercase_f_still_pages_down() {
205        assert_eq!(translate(key(KeyCode::Char('f'), KeyModifiers::NONE)), Command::PageDown);
206    }
207
208    #[test]
209    fn slash_opens_forward_search() {
210        assert_eq!(translate(key(KeyCode::Char('/'), KeyModifiers::NONE)), Command::SearchForward);
211    }
212
213    #[test]
214    fn question_mark_opens_backward_search() {
215        // `?` arrives as Char('?') with SHIFT on most layouts.
216        assert_eq!(translate(key(KeyCode::Char('?'), KeyModifiers::SHIFT)), Command::SearchBackward);
217    }
218
219    #[test]
220    fn n_repeats_match_forward() {
221        assert_eq!(translate(key(KeyCode::Char('n'), KeyModifiers::NONE)), Command::NextMatch);
222    }
223
224    #[test]
225    fn capital_n_repeats_match_backward() {
226        assert_eq!(translate(key(KeyCode::Char('N'), KeyModifiers::SHIFT)), Command::PreviousMatch);
227    }
228
229    #[test]
230    fn capital_r_triggers_reload() {
231        assert_eq!(translate(key(KeyCode::Char('R'), KeyModifiers::SHIFT)), Command::Reload);
232    }
233
234    #[test]
235    fn lowercase_r_still_refreshes() {
236        assert_eq!(translate(key(KeyCode::Char('r'), KeyModifiers::NONE)), Command::Refresh);
237    }
238
239    #[test]
240    fn capital_p_toggles_prettify() {
241        assert_eq!(translate(key(KeyCode::Char('P'), KeyModifiers::SHIFT)), Command::TogglePrettify);
242    }
243
244    #[test]
245    fn lowercase_p_remains_unbound() {
246        assert_eq!(translate(key(KeyCode::Char('p'), KeyModifiers::NONE)), Command::Noop);
247    }
248
249    #[test]
250    fn dash_is_option_prefix() {
251        assert_eq!(translate(key(KeyCode::Char('-'), KeyModifiers::NONE)), Command::OptionPrefix);
252    }
253
254    #[test]
255    fn resize_event() {
256        assert_eq!(translate(Event::Resize(80, 24)), Command::Resize(80, 24));
257    }
258
259    #[test]
260    fn m_key_produces_mark_set_command() {
261        let evt = key(KeyCode::Char('m'), KeyModifiers::NONE);
262        assert_eq!(translate(evt), Command::MarkSet);
263    }
264
265    #[test]
266    fn single_quote_key_produces_mark_jump_command() {
267        let evt = key(KeyCode::Char('\''), KeyModifiers::NONE);
268        assert_eq!(translate(evt), Command::MarkJump);
269    }
270
271    #[test]
272    fn ctrl_x_produces_ctrl_x_prefix_command() {
273        let evt = key(KeyCode::Char('x'), KeyModifiers::CONTROL);
274        assert_eq!(translate(evt), Command::CtrlXPrefix);
275    }
276}