1use super::Msg;
4use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5
6#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7pub enum ModalKind {
8 None,
9 DeleteConfirm,
10 ActiveMark,
11 FilterEdit,
12}
13
14pub fn key_to_msg(k: KeyEvent, modal: ModalKind, focus_right: bool) -> Option<Msg> {
15 use KeyCode::*;
16 match modal {
17 ModalKind::DeleteConfirm => match (k.code, k.modifiers) {
18 (Char('y'), KeyModifiers::NONE) | (Enter, _) => Some(Msg::ConfirmDelete),
19 (Char('n'), KeyModifiers::NONE) | (Esc, _) => Some(Msg::CancelDelete),
20 _ => None,
21 },
22 ModalKind::ActiveMark => match (k.code, k.modifiers) {
23 (Char('y'), KeyModifiers::NONE) | (Enter, _) => Some(Msg::ConfirmActiveMark),
24 (Char('n'), KeyModifiers::NONE) | (Esc, _) => Some(Msg::CancelActiveMark),
25 _ => None,
26 },
27 ModalKind::FilterEdit => match (k.code, k.modifiers) {
28 (Enter, _) => Some(Msg::FilterApply),
29 (Esc, _) => Some(Msg::FilterCancel),
30 (Backspace, _) => Some(Msg::FilterBackspace),
31 (Char(c), m)
33 if (m == KeyModifiers::NONE || m == KeyModifiers::SHIFT) && !c.is_control() =>
34 {
35 Some(Msg::FilterChar(c))
36 }
37 _ => None,
38 },
39 ModalKind::None if focus_right => match (k.code, k.modifiers) {
40 (Up, _) | (Char('k'), KeyModifiers::NONE) => Some(Msg::MoveUp),
43 (Down, _) | (Char('j'), KeyModifiers::NONE) => Some(Msg::MoveDown),
44 (Tab, _) | (BackTab, _) | (Esc, _) => Some(Msg::ToggleFocus),
45 (Char('q'), KeyModifiers::NONE) => Some(Msg::RequestQuit),
46 _ => None,
47 },
48 ModalKind::None => match (k.code, k.modifiers) {
49 (Up, _) | (Char('k'), KeyModifiers::NONE) => Some(Msg::MoveUp),
50 (Down, _) | (Char('j'), KeyModifiers::NONE) => Some(Msg::MoveDown),
51 (Right, _) | (Char('l'), KeyModifiers::NONE) | (Enter, _) => Some(Msg::DrillIn),
52 (Left, _) | (Char('h'), KeyModifiers::NONE) | (Esc, _) | (Backspace, _) => {
53 Some(Msg::DrillOut)
54 }
55 (Char(' '), _) => Some(Msg::ToggleMark),
56 (Char('m'), KeyModifiers::NONE) => Some(Msg::MarkDownToCursor),
57 (Char('s'), KeyModifiers::NONE) => Some(Msg::CycleSort),
58 (Char('d'), KeyModifiers::NONE) => Some(Msg::DeletePressed),
59 (Char('/'), KeyModifiers::NONE) => Some(Msg::FilterStart),
60 (Char('*'), _) => Some(Msg::MarkAllVisible),
61 (Tab, _) | (BackTab, _) => Some(Msg::ToggleFocus),
62 (Char('q'), KeyModifiers::NONE) => Some(Msg::RequestQuit),
63 _ => None,
64 },
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
72
73 fn k(code: KeyCode) -> KeyEvent {
74 KeyEvent::new(code, KeyModifiers::NONE)
75 }
76
77 #[test]
78 fn arrows_map_to_movement() {
79 assert!(matches!(
80 key_to_msg(k(KeyCode::Up), ModalKind::None, false),
81 Some(Msg::MoveUp)
82 ));
83 assert!(matches!(
84 key_to_msg(k(KeyCode::Down), ModalKind::None, false),
85 Some(Msg::MoveDown)
86 ));
87 }
88 #[test]
89 fn vim_keys_map_to_movement() {
90 assert!(matches!(
91 key_to_msg(k(KeyCode::Char('j')), ModalKind::None, false),
92 Some(Msg::MoveDown)
93 ));
94 assert!(matches!(
95 key_to_msg(k(KeyCode::Char('k')), ModalKind::None, false),
96 Some(Msg::MoveUp)
97 ));
98 assert!(matches!(
99 key_to_msg(k(KeyCode::Char('l')), ModalKind::None, false),
100 Some(Msg::DrillIn)
101 ));
102 assert!(matches!(
103 key_to_msg(k(KeyCode::Char('h')), ModalKind::None, false),
104 Some(Msg::DrillOut)
105 ));
106 }
107 #[test]
108 fn enter_is_drill_in() {
109 assert!(matches!(
110 key_to_msg(k(KeyCode::Enter), ModalKind::None, false),
111 Some(Msg::DrillIn)
112 ));
113 }
114 #[test]
115 fn esc_is_drill_out() {
116 assert!(matches!(
117 key_to_msg(k(KeyCode::Esc), ModalKind::None, false),
118 Some(Msg::DrillOut)
119 ));
120 }
121 #[test]
122 fn backspace_is_drill_out() {
123 assert!(matches!(
124 key_to_msg(k(KeyCode::Backspace), ModalKind::None, false),
125 Some(Msg::DrillOut)
126 ));
127 }
128 #[test]
129 fn q_requests_quit() {
130 assert!(matches!(
131 key_to_msg(k(KeyCode::Char('q')), ModalKind::None, false),
132 Some(Msg::RequestQuit)
133 ));
134 }
135 #[test]
136 fn d_in_normal_mode_requests_delete() {
137 assert!(matches!(
138 key_to_msg(k(KeyCode::Char('d')), ModalKind::None, false),
139 Some(Msg::DeletePressed)
140 ));
141 }
142 #[test]
143 fn y_in_modal_confirms_delete() {
144 assert!(matches!(
145 key_to_msg(k(KeyCode::Char('y')), ModalKind::DeleteConfirm, false),
146 Some(Msg::ConfirmDelete)
147 ));
148 }
149 #[test]
150 fn n_in_modal_cancels_delete() {
151 assert!(matches!(
152 key_to_msg(k(KeyCode::Char('n')), ModalKind::DeleteConfirm, false),
153 Some(Msg::CancelDelete)
154 ));
155 }
156 #[test]
157 fn y_in_active_modal_confirms_active_mark() {
158 assert!(matches!(
159 key_to_msg(k(KeyCode::Char('y')), ModalKind::ActiveMark, false),
160 Some(Msg::ConfirmActiveMark)
161 ));
162 }
163 #[test]
164 fn n_in_active_modal_cancels_active_mark() {
165 assert!(matches!(
166 key_to_msg(k(KeyCode::Char('n')), ModalKind::ActiveMark, false),
167 Some(Msg::CancelActiveMark)
168 ));
169 }
170
171 #[test]
172 fn right_focus_swallows_mark_sort_delete() {
173 assert!(key_to_msg(k(KeyCode::Char(' ')), ModalKind::None, true).is_none());
177 assert!(key_to_msg(k(KeyCode::Char('m')), ModalKind::None, true).is_none());
178 assert!(key_to_msg(k(KeyCode::Char('s')), ModalKind::None, true).is_none());
179 assert!(key_to_msg(k(KeyCode::Char('d')), ModalKind::None, true).is_none());
180 }
181
182 #[test]
183 fn slash_starts_filter() {
184 assert!(matches!(
185 key_to_msg(k(KeyCode::Char('/')), ModalKind::None, false),
186 Some(Msg::FilterStart)
187 ));
188 }
189
190 #[test]
191 fn star_marks_all_visible() {
192 assert!(matches!(
193 key_to_msg(k(KeyCode::Char('*')), ModalKind::None, false),
194 Some(Msg::MarkAllVisible)
195 ));
196 }
197
198 #[test]
199 fn filter_edit_routes_text_input() {
200 assert!(matches!(
201 key_to_msg(k(KeyCode::Char('a')), ModalKind::FilterEdit, false),
202 Some(Msg::FilterChar('a'))
203 ));
204 assert!(matches!(
205 key_to_msg(k(KeyCode::Backspace), ModalKind::FilterEdit, false),
206 Some(Msg::FilterBackspace)
207 ));
208 assert!(matches!(
209 key_to_msg(k(KeyCode::Enter), ModalKind::FilterEdit, false),
210 Some(Msg::FilterApply)
211 ));
212 assert!(matches!(
213 key_to_msg(k(KeyCode::Esc), ModalKind::FilterEdit, false),
214 Some(Msg::FilterCancel)
215 ));
216 }
217
218 #[test]
219 fn right_focus_keeps_movement_focus_quit() {
220 assert!(matches!(
221 key_to_msg(k(KeyCode::Up), ModalKind::None, true),
222 Some(Msg::MoveUp)
223 ));
224 assert!(matches!(
225 key_to_msg(k(KeyCode::Down), ModalKind::None, true),
226 Some(Msg::MoveDown)
227 ));
228 assert!(matches!(
229 key_to_msg(k(KeyCode::Tab), ModalKind::None, true),
230 Some(Msg::ToggleFocus)
231 ));
232 assert!(matches!(
233 key_to_msg(k(KeyCode::BackTab), ModalKind::None, true),
234 Some(Msg::ToggleFocus)
235 ));
236 assert!(matches!(
237 key_to_msg(k(KeyCode::Esc), ModalKind::None, true),
238 Some(Msg::ToggleFocus)
239 ));
240 assert!(matches!(
241 key_to_msg(k(KeyCode::Char('q')), ModalKind::None, true),
242 Some(Msg::RequestQuit)
243 ));
244 }
245}