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    GoTop,
17    GoBottom,
18    Quit,
19    Resize(u16, u16),
20    Refresh,
21    ToggleLineNumbers,
22    ToggleChop,
23    ToggleFollow,
24    /// `/` — open the forward-search prompt.
25    SearchForward,
26    /// `?` — open the backward-search prompt.
27    SearchBackward,
28    /// `n` — repeat the last search in its original direction.
29    NextMatch,
30    /// `N` — repeat the last search in the opposite direction.
31    PreviousMatch,
32    /// `-` — option-toggle prefix: the next key chooses an option to flip
33    /// (`N` → line numbers, `S` → chop, `F` → follow).
34    OptionPrefix,
35    /// `R` — force-reload the source from disk now (only meaningful with
36    /// `--live`; no-op for static file sources and append-streaming follow).
37    Reload,
38    /// `Shift-P` — toggle pretty-printing on/off (cycles back to the last
39    /// active mode if currently off).
40    TogglePrettify,
41    /// Set a specific prettify mode (issued by the `-P<letter>` sub-prefix
42    /// after the user picks j/y/t/x/h/c).
43    SetPrettifyMode(PrettifyMode),
44    /// Re-run byte-based content detection and apply the result (`-Pa`).
45    RedetectPrettify,
46    Noop,
47}
48
49pub fn translate(event: Event) -> Command {
50    match event {
51        Event::Resize(c, r) => Command::Resize(c, r),
52        Event::Key(KeyEvent { code, modifiers, .. }) => translate_key(code, modifiers),
53        _ => Command::Noop,
54    }
55}
56
57fn translate_key(code: KeyCode, mods: KeyModifiers) -> Command {
58    use KeyCode::*;
59    let ctrl = mods.contains(KeyModifiers::CONTROL);
60    match (code, ctrl) {
61        (Char('q'), false) | (Char('Q'), false) => Command::Quit,
62        (Char('c'), true) => Command::Quit,
63        (Down, _) | (Char('j'), false) | (Char('e'), false) | (Char('e'), true) | (Enter, _) => Command::ScrollLines(1),
64        (Char('y'), false) | (Char('y'), true) | (Up, _) | (Char('k'), false) => Command::ScrollLines(-1),
65        (Char('J'), false) => Command::ScrollLogicalLines(1),
66        (Char('K'), false) => Command::ScrollLogicalLines(-1),
67        (Char(' '), false) | (Char('f'), false) | (Char('f'), true) | (PageDown, _) => Command::PageDown,
68        (Char('b'), false) | (Char('b'), true) | (PageUp, _) => Command::PageUp,
69        (Char('d'), false) | (Char('d'), true) => Command::HalfPageDown,
70        (Char('u'), false) | (Char('u'), true) => Command::HalfPageUp,
71        (Char('g'), false) | (Char('<'), false) | (Home, _) => Command::GoTop,
72        (Char('G'), false) | (Char('>'), false) | (End, _) => Command::GoBottom,
73        (Char('r'), false) | (Char('l'), true) => Command::Refresh,
74        (Char('R'), false) => Command::Reload,
75        (Char('P'), false) => Command::TogglePrettify,
76        (Char('-'), false) => Command::OptionPrefix,
77        (Char('F'), false) => Command::ToggleFollow,
78        (Char('/'), false) => Command::SearchForward,
79        (Char('?'), false) => Command::SearchBackward,
80        (Char('n'), false) => Command::NextMatch,
81        (Char('N'), false) => Command::PreviousMatch,
82        _ => Command::Noop,
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crossterm::event::{KeyCode, KeyEventKind, KeyEventState};
90
91    fn key(code: KeyCode, mods: KeyModifiers) -> Event {
92        Event::Key(KeyEvent {
93            code, modifiers: mods,
94            kind: KeyEventKind::Press, state: KeyEventState::NONE,
95        })
96    }
97
98    #[test]
99    fn arrow_down_scrolls_one() {
100        assert_eq!(translate(key(KeyCode::Down, KeyModifiers::NONE)), Command::ScrollLines(1));
101    }
102
103    #[test]
104    fn j_scrolls_one() {
105        assert_eq!(translate(key(KeyCode::Char('j'), KeyModifiers::NONE)), Command::ScrollLines(1));
106    }
107
108    #[test]
109    fn space_pages_down() {
110        assert_eq!(translate(key(KeyCode::Char(' '), KeyModifiers::NONE)), Command::PageDown);
111    }
112
113    #[test]
114    fn ctrl_c_quits() {
115        assert_eq!(translate(key(KeyCode::Char('c'), KeyModifiers::CONTROL)), Command::Quit);
116    }
117
118    #[test]
119    fn capital_g_goes_to_bottom() {
120        assert_eq!(translate(key(KeyCode::Char('G'), KeyModifiers::SHIFT)), Command::GoBottom);
121    }
122
123    #[test]
124    fn capital_j_jumps_one_logical_line_forward() {
125        assert_eq!(translate(key(KeyCode::Char('J'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(1));
126    }
127
128    #[test]
129    fn capital_k_jumps_one_logical_line_backward() {
130        assert_eq!(translate(key(KeyCode::Char('K'), KeyModifiers::SHIFT)), Command::ScrollLogicalLines(-1));
131    }
132
133    #[test]
134    fn capital_f_toggles_follow() {
135        assert_eq!(translate(key(KeyCode::Char('F'), KeyModifiers::SHIFT)), Command::ToggleFollow);
136    }
137
138    #[test]
139    fn lowercase_f_still_pages_down() {
140        assert_eq!(translate(key(KeyCode::Char('f'), KeyModifiers::NONE)), Command::PageDown);
141    }
142
143    #[test]
144    fn slash_opens_forward_search() {
145        assert_eq!(translate(key(KeyCode::Char('/'), KeyModifiers::NONE)), Command::SearchForward);
146    }
147
148    #[test]
149    fn question_mark_opens_backward_search() {
150        // `?` arrives as Char('?') with SHIFT on most layouts.
151        assert_eq!(translate(key(KeyCode::Char('?'), KeyModifiers::SHIFT)), Command::SearchBackward);
152    }
153
154    #[test]
155    fn n_repeats_match_forward() {
156        assert_eq!(translate(key(KeyCode::Char('n'), KeyModifiers::NONE)), Command::NextMatch);
157    }
158
159    #[test]
160    fn capital_n_repeats_match_backward() {
161        assert_eq!(translate(key(KeyCode::Char('N'), KeyModifiers::SHIFT)), Command::PreviousMatch);
162    }
163
164    #[test]
165    fn capital_r_triggers_reload() {
166        assert_eq!(translate(key(KeyCode::Char('R'), KeyModifiers::SHIFT)), Command::Reload);
167    }
168
169    #[test]
170    fn lowercase_r_still_refreshes() {
171        assert_eq!(translate(key(KeyCode::Char('r'), KeyModifiers::NONE)), Command::Refresh);
172    }
173
174    #[test]
175    fn capital_p_toggles_prettify() {
176        assert_eq!(translate(key(KeyCode::Char('P'), KeyModifiers::SHIFT)), Command::TogglePrettify);
177    }
178
179    #[test]
180    fn lowercase_p_remains_unbound() {
181        assert_eq!(translate(key(KeyCode::Char('p'), KeyModifiers::NONE)), Command::Noop);
182    }
183
184    #[test]
185    fn dash_is_option_prefix() {
186        assert_eq!(translate(key(KeyCode::Char('-'), KeyModifiers::NONE)), Command::OptionPrefix);
187    }
188
189    #[test]
190    fn resize_event() {
191        assert_eq!(translate(Event::Resize(80, 24)), Command::Resize(80, 24));
192    }
193}