1use crossterm::event::{KeyEvent, MouseEvent};
51use std::fmt;
52
53#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum EventResult {
59 Consumed,
61 Ignored,
63 NavigateTo(TabTarget),
65 Exit,
67 ShowOverlay(OverlayKind),
69 StatusMessage(String),
71}
72
73impl EventResult {
74 pub fn status(msg: impl Into<String>) -> Self {
76 Self::StatusMessage(msg.into())
77 }
78
79 #[must_use]
81 pub const fn navigate(target: TabTarget) -> Self {
82 Self::NavigateTo(target)
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum TabTarget {
89 Summary,
90 Overview,
91 Tree,
92 Components,
93 Dependencies,
94 Licenses,
95 Vulnerabilities,
96 Quality,
97 Compliance,
98 SideBySide,
99 GraphChanges,
100 Source,
101 ComponentByName(String),
103 VulnerabilityById(String),
105 ComponentByLicense(String),
107}
108
109impl TabTarget {
110 #[must_use]
112 pub const fn to_tab_kind(&self) -> Option<super::app::TabKind> {
113 match self {
114 Self::Summary => Some(super::app::TabKind::Summary),
115 Self::Overview => Some(super::app::TabKind::Overview),
116 Self::Tree => Some(super::app::TabKind::Tree),
117 Self::Components | Self::ComponentByName(_) | Self::ComponentByLicense(_) => {
118 Some(super::app::TabKind::Components)
119 }
120 Self::Dependencies => Some(super::app::TabKind::Dependencies),
121 Self::Licenses => Some(super::app::TabKind::Licenses),
122 Self::Vulnerabilities | Self::VulnerabilityById(_) => {
123 Some(super::app::TabKind::Vulnerabilities)
124 }
125 Self::Quality => Some(super::app::TabKind::Quality),
126 Self::Compliance => Some(super::app::TabKind::Compliance),
127 Self::SideBySide => Some(super::app::TabKind::SideBySide),
128 Self::GraphChanges => Some(super::app::TabKind::GraphChanges),
129 Self::Source => Some(super::app::TabKind::Source),
130 }
131 }
132
133 #[must_use]
135 pub const fn from_tab_kind(kind: super::app::TabKind) -> Self {
136 match kind {
137 super::app::TabKind::Summary => Self::Summary,
138 super::app::TabKind::Overview => Self::Overview,
139 super::app::TabKind::Tree => Self::Tree,
140 super::app::TabKind::Components => Self::Components,
141 super::app::TabKind::Dependencies => Self::Dependencies,
142 super::app::TabKind::Licenses => Self::Licenses,
143 super::app::TabKind::Vulnerabilities => Self::Vulnerabilities,
144 super::app::TabKind::Quality => Self::Quality,
145 super::app::TabKind::Compliance => Self::Compliance,
146 super::app::TabKind::SideBySide => Self::SideBySide,
147 super::app::TabKind::GraphChanges => Self::GraphChanges,
148 super::app::TabKind::Source => Self::Source,
149 }
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum OverlayKind {
156 Help,
157 Export,
158 Legend,
159 Search,
160 Shortcuts,
161}
162
163#[derive(Debug, Clone)]
165pub struct Shortcut {
166 pub key: String,
168 pub description: String,
170 pub primary: bool,
172}
173
174impl Shortcut {
175 pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
177 Self {
178 key: key.into(),
179 description: description.into(),
180 primary: false,
181 }
182 }
183
184 pub fn primary(key: impl Into<String>, description: impl Into<String>) -> Self {
186 Self {
187 key: key.into(),
188 description: description.into(),
189 primary: true,
190 }
191 }
192}
193
194pub struct ViewContext<'a> {
196 pub mode: ViewMode,
198 pub focused: bool,
200 pub width: u16,
202 pub height: u16,
204 pub tick: u64,
206 pub status_message: &'a mut Option<String>,
208}
209
210impl ViewContext<'_> {
211 pub fn set_status(&mut self, msg: impl Into<String>) {
213 *self.status_message = Some(msg.into());
214 }
215
216 pub fn clear_status(&mut self) {
218 *self.status_message = None;
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum ViewMode {
225 Diff,
227 View,
229 MultiDiff,
231 Timeline,
233 Matrix,
235}
236
237impl ViewMode {
238 #[must_use]
240 pub const fn from_app_mode(mode: super::app::AppMode) -> Self {
241 match mode {
242 super::app::AppMode::Diff => Self::Diff,
243 super::app::AppMode::View => Self::View,
244 super::app::AppMode::MultiDiff => Self::MultiDiff,
245 super::app::AppMode::Timeline => Self::Timeline,
246 super::app::AppMode::Matrix => Self::Matrix,
247 }
248 }
249}
250
251pub trait ViewState: Send {
277 fn handle_key(&mut self, key: KeyEvent, ctx: &mut ViewContext) -> EventResult;
283
284 fn handle_mouse(&mut self, _mouse: MouseEvent, _ctx: &mut ViewContext) -> EventResult {
288 EventResult::Ignored
289 }
290
291 fn title(&self) -> &str;
293
294 fn shortcuts(&self) -> Vec<Shortcut>;
303
304 fn on_enter(&mut self, _ctx: &mut ViewContext) {}
308
309 fn on_leave(&mut self, _ctx: &mut ViewContext) {}
313
314 fn on_tick(&mut self, _ctx: &mut ViewContext) {}
318
319 fn has_modal(&self) -> bool {
323 false
324 }
325}
326
327pub trait ListViewState: ViewState {
332 fn selected(&self) -> usize;
334
335 fn set_selected(&mut self, idx: usize);
337
338 fn total(&self) -> usize;
340
341 fn select_next(&mut self) {
343 let total = self.total();
344 let selected = self.selected();
345 if total > 0 && selected < total.saturating_sub(1) {
346 self.set_selected(selected + 1);
347 }
348 }
349
350 fn select_prev(&mut self) {
352 let selected = self.selected();
353 if selected > 0 {
354 self.set_selected(selected - 1);
355 }
356 }
357
358 fn page_down(&mut self) {
360 use super::constants::PAGE_SIZE;
361 let total = self.total();
362 let selected = self.selected();
363 if total > 0 {
364 self.set_selected((selected + PAGE_SIZE).min(total.saturating_sub(1)));
365 }
366 }
367
368 fn page_up(&mut self) {
370 use super::constants::PAGE_SIZE;
371 let selected = self.selected();
372 self.set_selected(selected.saturating_sub(PAGE_SIZE));
373 }
374
375 fn go_first(&mut self) {
377 self.set_selected(0);
378 }
379
380 fn go_last(&mut self) {
382 let total = self.total();
383 if total > 0 {
384 self.set_selected(total.saturating_sub(1));
385 }
386 }
387
388 fn handle_list_nav_key(&mut self, key: KeyEvent) -> EventResult {
397 use crossterm::event::KeyCode;
398
399 match key.code {
400 KeyCode::Down | KeyCode::Char('j') => {
401 self.select_next();
402 EventResult::Consumed
403 }
404 KeyCode::Up | KeyCode::Char('k') => {
405 self.select_prev();
406 EventResult::Consumed
407 }
408 KeyCode::Home | KeyCode::Char('g') => {
409 self.go_first();
410 EventResult::Consumed
411 }
412 KeyCode::End | KeyCode::Char('G') => {
413 self.go_last();
414 EventResult::Consumed
415 }
416 KeyCode::PageDown => {
417 self.page_down();
418 EventResult::Consumed
419 }
420 KeyCode::PageUp => {
421 self.page_up();
422 EventResult::Consumed
423 }
424 _ => EventResult::Ignored,
425 }
426 }
427}
428
429impl fmt::Display for EventResult {
431 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432 match self {
433 Self::Consumed => write!(f, "Consumed"),
434 Self::Ignored => write!(f, "Ignored"),
435 Self::NavigateTo(target) => write!(f, "NavigateTo({target:?})"),
436 Self::Exit => write!(f, "Exit"),
437 Self::ShowOverlay(kind) => write!(f, "ShowOverlay({kind:?})"),
438 Self::StatusMessage(msg) => write!(f, "StatusMessage({msg})"),
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crossterm::event::{KeyCode, KeyModifiers};
447
448 struct TestListView {
450 selected: usize,
451 total: usize,
452 }
453
454 impl TestListView {
455 fn new(total: usize) -> Self {
456 Self { selected: 0, total }
457 }
458 }
459
460 impl ViewState for TestListView {
461 fn handle_key(&mut self, key: KeyEvent, _ctx: &mut ViewContext) -> EventResult {
462 self.handle_list_nav_key(key)
463 }
464
465 fn title(&self) -> &str {
466 "Test View"
467 }
468
469 fn shortcuts(&self) -> Vec<Shortcut> {
470 vec![
471 Shortcut::primary("j/k", "Navigate"),
472 Shortcut::new("g/G", "First/Last"),
473 ]
474 }
475 }
476
477 impl ListViewState for TestListView {
478 fn selected(&self) -> usize {
479 self.selected
480 }
481
482 fn set_selected(&mut self, idx: usize) {
483 self.selected = idx;
484 }
485
486 fn total(&self) -> usize {
487 self.total
488 }
489 }
490
491 fn make_key_event(code: KeyCode) -> KeyEvent {
492 KeyEvent::new(code, KeyModifiers::empty())
493 }
494
495 fn make_context() -> ViewContext<'static> {
496 let status: &'static mut Option<String> = Box::leak(Box::new(None));
497 ViewContext {
498 mode: ViewMode::Diff,
499 focused: true,
500 width: 80,
501 height: 24,
502 tick: 0,
503 status_message: status,
504 }
505 }
506
507 #[test]
508 fn test_list_view_navigation() {
509 let mut view = TestListView::new(10);
510 let mut ctx = make_context();
511
512 assert_eq!(view.selected(), 0);
514
515 let result = view.handle_key(make_key_event(KeyCode::Down), &mut ctx);
517 assert_eq!(result, EventResult::Consumed);
518 assert_eq!(view.selected(), 1);
519
520 let result = view.handle_key(make_key_event(KeyCode::Up), &mut ctx);
522 assert_eq!(result, EventResult::Consumed);
523 assert_eq!(view.selected(), 0);
524
525 let result = view.handle_key(make_key_event(KeyCode::Up), &mut ctx);
527 assert_eq!(result, EventResult::Consumed);
528 assert_eq!(view.selected(), 0);
529 }
530
531 #[test]
532 fn test_list_view_go_to_end() {
533 let mut view = TestListView::new(10);
534 let mut ctx = make_context();
535
536 let result = view.handle_key(make_key_event(KeyCode::Char('G')), &mut ctx);
538 assert_eq!(result, EventResult::Consumed);
539 assert_eq!(view.selected(), 9);
540
541 let result = view.handle_key(make_key_event(KeyCode::Down), &mut ctx);
543 assert_eq!(result, EventResult::Consumed);
544 assert_eq!(view.selected(), 9);
545 }
546
547 #[test]
548 fn test_event_result_display() {
549 assert_eq!(format!("{}", EventResult::Consumed), "Consumed");
550 assert_eq!(format!("{}", EventResult::Ignored), "Ignored");
551 assert_eq!(format!("{}", EventResult::Exit), "Exit");
552 }
553
554 #[test]
555 fn test_shortcut_creation() {
556 let shortcut = Shortcut::new("Enter", "Select item");
557 assert_eq!(shortcut.key, "Enter");
558 assert_eq!(shortcut.description, "Select item");
559 assert!(!shortcut.primary);
560
561 let primary = Shortcut::primary("q", "Quit");
562 assert!(primary.primary);
563 }
564
565 #[test]
566 fn test_event_result_helpers() {
567 let result = EventResult::status("Test message");
568 assert_eq!(
569 result,
570 EventResult::StatusMessage("Test message".to_string())
571 );
572
573 let nav = EventResult::navigate(TabTarget::Components);
574 assert_eq!(nav, EventResult::NavigateTo(TabTarget::Components));
575 }
576}