1use crossterm::event::{KeyCode, KeyModifiers, MouseEvent};
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum Msg {
19 MoveUp,
22 MoveDown,
24 PageUp,
26 PageDown,
28 JumpToTop,
30 JumpToBottom,
32 SelectIndex(usize),
34
35 NextView,
38 PrevView,
40 SwitchToView(ViewKind),
42
43 CycleTypeFilter,
46 CycleBranchFilter,
48 OpenBranchSearch,
50 SetSearchQuery(String),
52 ClearFilters,
54
55 SearchInput(char),
58 SearchBackspace,
60 SearchConfirm,
62 SearchCancel,
64
65 ToggleDetailPanel,
68 DetailScrollUp,
70 DetailScrollDown,
72
73 ToggleHelp,
76 OpenPromptModal,
78 CloseModal,
80 ModalScrollUp,
82 ModalScrollDown,
84
85 ToggleFileBrowser,
88 FileBrowserEnter,
90 FileBrowserBack,
92 FileBrowserToggle,
94 PreviewFile,
96 ShowFileDiff,
98
99 ToggleGoalStory,
102 GoalStoryToggle,
104
105 OpenFiles,
108 RefreshGraph,
110 CopyToClipboard,
112
113 Quit,
116 Tick,
118 Resize(u16, u16),
120 Mouse(MouseEvent),
122
123 Noop,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum ViewKind {
131 Timeline,
132 Dag,
133 Graph,
134}
135
136impl ViewKind {
137 pub fn next(self) -> Self {
138 match self {
139 ViewKind::Timeline => ViewKind::Dag,
140 ViewKind::Dag => ViewKind::Graph,
141 ViewKind::Graph => ViewKind::Timeline,
142 }
143 }
144
145 pub fn prev(self) -> Self {
146 match self {
147 ViewKind::Timeline => ViewKind::Graph,
148 ViewKind::Dag => ViewKind::Timeline,
149 ViewKind::Graph => ViewKind::Dag,
150 }
151 }
152}
153
154pub fn key_to_msg(
159 code: KeyCode,
160 modifiers: KeyModifiers,
161 modal_open: bool,
162 search_active: bool,
163) -> Msg {
164 if search_active {
166 return match code {
167 KeyCode::Enter => Msg::SearchConfirm,
168 KeyCode::Esc => Msg::SearchCancel,
169 KeyCode::Backspace => Msg::SearchBackspace,
170 KeyCode::Char(c) => Msg::SearchInput(c),
171 _ => Msg::Noop,
172 };
173 }
174
175 if modal_open {
177 return match code {
178 KeyCode::Esc | KeyCode::Char('q') => Msg::CloseModal,
179 KeyCode::Char('j') | KeyCode::Down => Msg::ModalScrollDown,
180 KeyCode::Char('k') | KeyCode::Up => Msg::ModalScrollUp,
181 KeyCode::Char('d') if modifiers.contains(KeyModifiers::CONTROL) => Msg::ModalScrollDown,
182 KeyCode::Char('u') if modifiers.contains(KeyModifiers::CONTROL) => Msg::ModalScrollUp,
183 _ => Msg::Noop,
184 };
185 }
186
187 match code {
189 KeyCode::Char('q') => Msg::Quit,
191 KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => Msg::Quit,
192
193 KeyCode::Char('j') | KeyCode::Down => Msg::MoveDown,
195 KeyCode::Char('k') | KeyCode::Up => Msg::MoveUp,
196 KeyCode::Char('d') if modifiers.contains(KeyModifiers::CONTROL) => Msg::PageDown,
197 KeyCode::Char('u') if modifiers.contains(KeyModifiers::CONTROL) => Msg::PageUp,
198 KeyCode::Char('g') => Msg::JumpToTop, KeyCode::Char('G') => Msg::JumpToBottom,
200 KeyCode::PageDown => Msg::PageDown,
201 KeyCode::PageUp => Msg::PageUp,
202 KeyCode::Home => Msg::JumpToTop,
203 KeyCode::End => Msg::JumpToBottom,
204
205 KeyCode::Tab => {
207 if modifiers.contains(KeyModifiers::SHIFT) {
208 Msg::PrevView
209 } else {
210 Msg::NextView
211 }
212 }
213 KeyCode::Char('1') => Msg::SwitchToView(ViewKind::Timeline),
214 KeyCode::Char('2') => Msg::SwitchToView(ViewKind::Dag),
215 KeyCode::Char('3') => Msg::SwitchToView(ViewKind::Graph),
216
217 KeyCode::Char('t') => Msg::CycleTypeFilter,
219 KeyCode::Char('b') => Msg::CycleBranchFilter,
220 KeyCode::Char('B') => Msg::OpenBranchSearch,
221 KeyCode::Char('/') => Msg::OpenBranchSearch, KeyCode::Char('l') | KeyCode::Right => Msg::DetailScrollDown, KeyCode::Char('h') | KeyCode::Left => Msg::DetailScrollUp, KeyCode::Enter => Msg::ToggleDetailPanel,
227
228 KeyCode::Char('?') => Msg::ToggleHelp,
230 KeyCode::Char('P') => Msg::OpenPromptModal,
231 KeyCode::Esc => Msg::CloseModal,
232
233 KeyCode::Char('F') => Msg::ToggleFileBrowser,
235 KeyCode::Char('p') => Msg::PreviewFile,
236
237 KeyCode::Char('s') => Msg::ToggleGoalStory,
239
240 KeyCode::Char('o') => Msg::OpenFiles,
242 KeyCode::Char('r') => Msg::RefreshGraph,
243 KeyCode::Char('y') => Msg::CopyToClipboard,
244
245 _ => Msg::Noop,
246 }
247}
248
249pub fn is_quit(msg: &Msg) -> bool {
251 matches!(msg, Msg::Quit)
252}
253
254pub fn is_navigation(msg: &Msg) -> bool {
256 matches!(
257 msg,
258 Msg::MoveUp
259 | Msg::MoveDown
260 | Msg::PageUp
261 | Msg::PageDown
262 | Msg::JumpToTop
263 | Msg::JumpToBottom
264 | Msg::SelectIndex(_)
265 )
266}
267
268pub fn is_filter_change(msg: &Msg) -> bool {
270 matches!(
271 msg,
272 Msg::CycleTypeFilter
273 | Msg::CycleBranchFilter
274 | Msg::SetSearchQuery(_)
275 | Msg::ClearFilters
276 | Msg::SearchConfirm
277 )
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_view_kind_cycle() {
286 assert_eq!(ViewKind::Timeline.next(), ViewKind::Dag);
287 assert_eq!(ViewKind::Dag.next(), ViewKind::Graph);
288 assert_eq!(ViewKind::Graph.next(), ViewKind::Timeline);
289
290 assert_eq!(ViewKind::Timeline.prev(), ViewKind::Graph);
291 assert_eq!(ViewKind::Graph.prev(), ViewKind::Dag);
292 }
293
294 #[test]
295 fn test_key_to_msg_navigation() {
296 assert_eq!(
297 key_to_msg(KeyCode::Char('j'), KeyModifiers::NONE, false, false),
298 Msg::MoveDown
299 );
300 assert_eq!(
301 key_to_msg(KeyCode::Char('k'), KeyModifiers::NONE, false, false),
302 Msg::MoveUp
303 );
304 assert_eq!(
305 key_to_msg(KeyCode::Down, KeyModifiers::NONE, false, false),
306 Msg::MoveDown
307 );
308 assert_eq!(
309 key_to_msg(KeyCode::Up, KeyModifiers::NONE, false, false),
310 Msg::MoveUp
311 );
312 }
313
314 #[test]
315 fn test_key_to_msg_quit() {
316 assert_eq!(
317 key_to_msg(KeyCode::Char('q'), KeyModifiers::NONE, false, false),
318 Msg::Quit
319 );
320 assert_eq!(
321 key_to_msg(KeyCode::Char('c'), KeyModifiers::CONTROL, false, false),
322 Msg::Quit
323 );
324 }
325
326 #[test]
327 fn test_key_to_msg_search_mode() {
328 assert_eq!(
329 key_to_msg(KeyCode::Char('a'), KeyModifiers::NONE, false, true),
330 Msg::SearchInput('a')
331 );
332 assert_eq!(
333 key_to_msg(KeyCode::Enter, KeyModifiers::NONE, false, true),
334 Msg::SearchConfirm
335 );
336 assert_eq!(
337 key_to_msg(KeyCode::Esc, KeyModifiers::NONE, false, true),
338 Msg::SearchCancel
339 );
340 assert_eq!(
341 key_to_msg(KeyCode::Backspace, KeyModifiers::NONE, false, true),
342 Msg::SearchBackspace
343 );
344 }
345
346 #[test]
347 fn test_key_to_msg_modal_mode() {
348 assert_eq!(
349 key_to_msg(KeyCode::Char('j'), KeyModifiers::NONE, true, false),
350 Msg::ModalScrollDown
351 );
352 assert_eq!(
353 key_to_msg(KeyCode::Char('k'), KeyModifiers::NONE, true, false),
354 Msg::ModalScrollUp
355 );
356 assert_eq!(
357 key_to_msg(KeyCode::Esc, KeyModifiers::NONE, true, false),
358 Msg::CloseModal
359 );
360 }
361
362 #[test]
363 fn test_is_quit() {
364 assert!(is_quit(&Msg::Quit));
365 assert!(!is_quit(&Msg::MoveDown));
366 }
367
368 #[test]
369 fn test_is_navigation() {
370 assert!(is_navigation(&Msg::MoveUp));
371 assert!(is_navigation(&Msg::MoveDown));
372 assert!(is_navigation(&Msg::PageUp));
373 assert!(!is_navigation(&Msg::Quit));
374 assert!(!is_navigation(&Msg::ToggleHelp));
375 }
376
377 #[test]
378 fn test_is_filter_change() {
379 assert!(is_filter_change(&Msg::CycleTypeFilter));
380 assert!(is_filter_change(&Msg::CycleBranchFilter));
381 assert!(!is_filter_change(&Msg::MoveUp));
382 }
383
384 #[test]
385 fn test_key_to_msg_actions() {
386 assert_eq!(
388 key_to_msg(KeyCode::Char('o'), KeyModifiers::NONE, false, false),
389 Msg::OpenFiles
390 );
391 assert_eq!(
392 key_to_msg(KeyCode::Char('r'), KeyModifiers::NONE, false, false),
393 Msg::RefreshGraph
394 );
395 assert_eq!(
396 key_to_msg(KeyCode::Char('y'), KeyModifiers::NONE, false, false),
397 Msg::CopyToClipboard
398 );
399 }
400
401 #[test]
402 fn test_key_to_msg_view_switching() {
403 assert_eq!(
404 key_to_msg(KeyCode::Tab, KeyModifiers::NONE, false, false),
405 Msg::NextView
406 );
407 assert_eq!(
408 key_to_msg(KeyCode::Tab, KeyModifiers::SHIFT, false, false),
409 Msg::PrevView
410 );
411 assert_eq!(
412 key_to_msg(KeyCode::Char('1'), KeyModifiers::NONE, false, false),
413 Msg::SwitchToView(ViewKind::Timeline)
414 );
415 assert_eq!(
416 key_to_msg(KeyCode::Char('2'), KeyModifiers::NONE, false, false),
417 Msg::SwitchToView(ViewKind::Dag)
418 );
419 assert_eq!(
420 key_to_msg(KeyCode::Char('3'), KeyModifiers::NONE, false, false),
421 Msg::SwitchToView(ViewKind::Graph)
422 );
423 }
424
425 #[test]
426 fn test_key_to_msg_filtering() {
427 assert_eq!(
428 key_to_msg(KeyCode::Char('t'), KeyModifiers::NONE, false, false),
429 Msg::CycleTypeFilter
430 );
431 assert_eq!(
432 key_to_msg(KeyCode::Char('b'), KeyModifiers::NONE, false, false),
433 Msg::CycleBranchFilter
434 );
435 assert_eq!(
436 key_to_msg(KeyCode::Char('B'), KeyModifiers::NONE, false, false),
437 Msg::OpenBranchSearch
438 );
439 assert_eq!(
440 key_to_msg(KeyCode::Char('/'), KeyModifiers::NONE, false, false),
441 Msg::OpenBranchSearch
442 );
443 }
444
445 #[test]
446 fn test_key_to_msg_modals() {
447 assert_eq!(
448 key_to_msg(KeyCode::Char('?'), KeyModifiers::NONE, false, false),
449 Msg::ToggleHelp
450 );
451 assert_eq!(
452 key_to_msg(KeyCode::Char('P'), KeyModifiers::NONE, false, false),
453 Msg::OpenPromptModal
454 );
455 assert_eq!(
456 key_to_msg(KeyCode::Esc, KeyModifiers::NONE, false, false),
457 Msg::CloseModal
458 );
459 }
460
461 #[test]
462 fn test_key_to_msg_file_browser() {
463 assert_eq!(
464 key_to_msg(KeyCode::Char('F'), KeyModifiers::NONE, false, false),
465 Msg::ToggleFileBrowser
466 );
467 assert_eq!(
468 key_to_msg(KeyCode::Char('p'), KeyModifiers::NONE, false, false),
469 Msg::PreviewFile
470 );
471 }
472
473 #[test]
474 fn test_key_to_msg_detail_panel() {
475 assert_eq!(
476 key_to_msg(KeyCode::Enter, KeyModifiers::NONE, false, false),
477 Msg::ToggleDetailPanel
478 );
479 assert_eq!(
480 key_to_msg(KeyCode::Char('l'), KeyModifiers::NONE, false, false),
481 Msg::DetailScrollDown
482 );
483 assert_eq!(
484 key_to_msg(KeyCode::Char('h'), KeyModifiers::NONE, false, false),
485 Msg::DetailScrollUp
486 );
487 }
488
489 #[test]
490 fn test_key_to_msg_page_navigation() {
491 assert_eq!(
492 key_to_msg(KeyCode::Char('d'), KeyModifiers::CONTROL, false, false),
493 Msg::PageDown
494 );
495 assert_eq!(
496 key_to_msg(KeyCode::Char('u'), KeyModifiers::CONTROL, false, false),
497 Msg::PageUp
498 );
499 assert_eq!(
500 key_to_msg(KeyCode::PageDown, KeyModifiers::NONE, false, false),
501 Msg::PageDown
502 );
503 assert_eq!(
504 key_to_msg(KeyCode::PageUp, KeyModifiers::NONE, false, false),
505 Msg::PageUp
506 );
507 assert_eq!(
508 key_to_msg(KeyCode::Char('g'), KeyModifiers::NONE, false, false),
509 Msg::JumpToTop
510 );
511 assert_eq!(
512 key_to_msg(KeyCode::Char('G'), KeyModifiers::NONE, false, false),
513 Msg::JumpToBottom
514 );
515 assert_eq!(
516 key_to_msg(KeyCode::Home, KeyModifiers::NONE, false, false),
517 Msg::JumpToTop
518 );
519 assert_eq!(
520 key_to_msg(KeyCode::End, KeyModifiers::NONE, false, false),
521 Msg::JumpToBottom
522 );
523 }
524
525 #[test]
526 fn test_key_to_msg_goal_story() {
527 assert_eq!(
528 key_to_msg(KeyCode::Char('s'), KeyModifiers::NONE, false, false),
529 Msg::ToggleGoalStory
530 );
531 }
532
533 #[test]
534 fn test_key_to_msg_unhandled() {
535 assert_eq!(
537 key_to_msg(KeyCode::Char('z'), KeyModifiers::NONE, false, false),
538 Msg::Noop
539 );
540 assert_eq!(
541 key_to_msg(KeyCode::Char('x'), KeyModifiers::NONE, false, false),
542 Msg::Noop
543 );
544 }
545}