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 Components,
91 Dependencies,
92 Licenses,
93 Vulnerabilities,
94 Quality,
95 Compliance,
96 SideBySide,
97 GraphChanges,
98 Source,
99 ComponentByName(String),
101 VulnerabilityById(String),
103 ComponentByLicense(String),
105}
106
107impl TabTarget {
108 #[must_use]
110 pub const fn to_tab_kind(&self) -> Option<super::app::TabKind> {
111 match self {
112 Self::Summary => Some(super::app::TabKind::Summary),
113 Self::Components | Self::ComponentByName(_) | Self::ComponentByLicense(_) => {
114 Some(super::app::TabKind::Components)
115 }
116 Self::Dependencies => Some(super::app::TabKind::Dependencies),
117 Self::Licenses => Some(super::app::TabKind::Licenses),
118 Self::Vulnerabilities | Self::VulnerabilityById(_) => {
119 Some(super::app::TabKind::Vulnerabilities)
120 }
121 Self::Quality => Some(super::app::TabKind::Quality),
122 Self::Compliance => Some(super::app::TabKind::Compliance),
123 Self::SideBySide => Some(super::app::TabKind::SideBySide),
124 Self::GraphChanges => Some(super::app::TabKind::GraphChanges),
125 Self::Source => Some(super::app::TabKind::Source),
126 }
127 }
128
129 #[must_use]
131 pub const fn from_tab_kind(kind: super::app::TabKind) -> Self {
132 match kind {
133 super::app::TabKind::Summary => Self::Summary,
134 super::app::TabKind::Components => Self::Components,
135 super::app::TabKind::Dependencies => Self::Dependencies,
136 super::app::TabKind::Licenses => Self::Licenses,
137 super::app::TabKind::Vulnerabilities => Self::Vulnerabilities,
138 super::app::TabKind::Quality => Self::Quality,
139 super::app::TabKind::Compliance => Self::Compliance,
140 super::app::TabKind::SideBySide => Self::SideBySide,
141 super::app::TabKind::GraphChanges => Self::GraphChanges,
142 super::app::TabKind::Source => Self::Source,
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum OverlayKind {
150 Help,
151 Export,
152 Legend,
153 Search,
154 Shortcuts,
155}
156
157#[derive(Debug, Clone)]
159pub struct Shortcut {
160 pub key: String,
162 pub description: String,
164 pub primary: bool,
166}
167
168impl Shortcut {
169 pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
171 Self {
172 key: key.into(),
173 description: description.into(),
174 primary: false,
175 }
176 }
177
178 pub fn primary(key: impl Into<String>, description: impl Into<String>) -> Self {
180 Self {
181 key: key.into(),
182 description: description.into(),
183 primary: true,
184 }
185 }
186}
187
188pub struct ViewContext<'a> {
190 pub mode: ViewMode,
192 pub focused: bool,
194 pub width: u16,
196 pub height: u16,
198 pub tick: u64,
200 pub status_message: &'a mut Option<String>,
202}
203
204impl ViewContext<'_> {
205 pub fn set_status(&mut self, msg: impl Into<String>) {
207 *self.status_message = Some(msg.into());
208 }
209
210 pub fn clear_status(&mut self) {
212 *self.status_message = None;
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum ViewMode {
219 Diff,
221 View,
223 MultiDiff,
225 Timeline,
227 Matrix,
229}
230
231impl ViewMode {
232 #[must_use]
234 pub const fn from_app_mode(mode: super::app::AppMode) -> Self {
235 match mode {
236 super::app::AppMode::Diff => Self::Diff,
237 super::app::AppMode::MultiDiff => Self::MultiDiff,
238 super::app::AppMode::Timeline => Self::Timeline,
239 super::app::AppMode::Matrix => Self::Matrix,
240 }
241 }
242}
243
244pub trait ViewState: Send {
270 fn handle_key(&mut self, key: KeyEvent, ctx: &mut ViewContext) -> EventResult;
276
277 fn handle_mouse(&mut self, _mouse: MouseEvent, _ctx: &mut ViewContext) -> EventResult {
281 EventResult::Ignored
282 }
283
284 fn title(&self) -> &str;
286
287 fn shortcuts(&self) -> Vec<Shortcut>;
296
297 fn on_enter(&mut self, _ctx: &mut ViewContext) {}
301
302 fn on_leave(&mut self, _ctx: &mut ViewContext) {}
306
307 fn on_tick(&mut self, _ctx: &mut ViewContext) {}
311
312 fn has_modal(&self) -> bool {
316 false
317 }
318}
319
320pub trait ListViewState: ViewState {
325 fn selected(&self) -> usize;
327
328 fn set_selected(&mut self, idx: usize);
330
331 fn total(&self) -> usize;
333
334 fn select_next(&mut self) {
336 let total = self.total();
337 let selected = self.selected();
338 if total > 0 && selected < total.saturating_sub(1) {
339 self.set_selected(selected + 1);
340 }
341 }
342
343 fn select_prev(&mut self) {
345 let selected = self.selected();
346 if selected > 0 {
347 self.set_selected(selected - 1);
348 }
349 }
350
351 fn page_down(&mut self) {
353 use super::constants::PAGE_SIZE;
354 let total = self.total();
355 let selected = self.selected();
356 if total > 0 {
357 self.set_selected((selected + PAGE_SIZE).min(total.saturating_sub(1)));
358 }
359 }
360
361 fn page_up(&mut self) {
363 use super::constants::PAGE_SIZE;
364 let selected = self.selected();
365 self.set_selected(selected.saturating_sub(PAGE_SIZE));
366 }
367
368 fn go_first(&mut self) {
370 self.set_selected(0);
371 }
372
373 fn go_last(&mut self) {
375 let total = self.total();
376 if total > 0 {
377 self.set_selected(total.saturating_sub(1));
378 }
379 }
380
381 fn handle_list_nav_key(&mut self, key: KeyEvent) -> EventResult {
390 use crossterm::event::KeyCode;
391
392 match key.code {
393 KeyCode::Down | KeyCode::Char('j') => {
394 self.select_next();
395 EventResult::Consumed
396 }
397 KeyCode::Up | KeyCode::Char('k') => {
398 self.select_prev();
399 EventResult::Consumed
400 }
401 KeyCode::Home | KeyCode::Char('g') => {
402 self.go_first();
403 EventResult::Consumed
404 }
405 KeyCode::End | KeyCode::Char('G') => {
406 self.go_last();
407 EventResult::Consumed
408 }
409 KeyCode::PageDown => {
410 self.page_down();
411 EventResult::Consumed
412 }
413 KeyCode::PageUp => {
414 self.page_up();
415 EventResult::Consumed
416 }
417 _ => EventResult::Ignored,
418 }
419 }
420}
421
422impl fmt::Display for EventResult {
424 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 match self {
426 Self::Consumed => write!(f, "Consumed"),
427 Self::Ignored => write!(f, "Ignored"),
428 Self::NavigateTo(target) => write!(f, "NavigateTo({target:?})"),
429 Self::Exit => write!(f, "Exit"),
430 Self::ShowOverlay(kind) => write!(f, "ShowOverlay({kind:?})"),
431 Self::StatusMessage(msg) => write!(f, "StatusMessage({msg})"),
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crossterm::event::{KeyCode, KeyModifiers};
440
441 struct TestListView {
443 selected: usize,
444 total: usize,
445 }
446
447 impl TestListView {
448 fn new(total: usize) -> Self {
449 Self { selected: 0, total }
450 }
451 }
452
453 impl ViewState for TestListView {
454 fn handle_key(&mut self, key: KeyEvent, _ctx: &mut ViewContext) -> EventResult {
455 self.handle_list_nav_key(key)
456 }
457
458 fn title(&self) -> &str {
459 "Test View"
460 }
461
462 fn shortcuts(&self) -> Vec<Shortcut> {
463 vec![
464 Shortcut::primary("j/k", "Navigate"),
465 Shortcut::new("g/G", "First/Last"),
466 ]
467 }
468 }
469
470 impl ListViewState for TestListView {
471 fn selected(&self) -> usize {
472 self.selected
473 }
474
475 fn set_selected(&mut self, idx: usize) {
476 self.selected = idx;
477 }
478
479 fn total(&self) -> usize {
480 self.total
481 }
482 }
483
484 fn make_key_event(code: KeyCode) -> KeyEvent {
485 KeyEvent::new(code, KeyModifiers::empty())
486 }
487
488 fn make_context() -> ViewContext<'static> {
489 let status: &'static mut Option<String> = Box::leak(Box::new(None));
490 ViewContext {
491 mode: ViewMode::Diff,
492 focused: true,
493 width: 80,
494 height: 24,
495 tick: 0,
496 status_message: status,
497 }
498 }
499
500 #[test]
501 fn test_list_view_navigation() {
502 let mut view = TestListView::new(10);
503 let mut ctx = make_context();
504
505 assert_eq!(view.selected(), 0);
507
508 let result = view.handle_key(make_key_event(KeyCode::Down), &mut ctx);
510 assert_eq!(result, EventResult::Consumed);
511 assert_eq!(view.selected(), 1);
512
513 let result = view.handle_key(make_key_event(KeyCode::Up), &mut ctx);
515 assert_eq!(result, EventResult::Consumed);
516 assert_eq!(view.selected(), 0);
517
518 let result = view.handle_key(make_key_event(KeyCode::Up), &mut ctx);
520 assert_eq!(result, EventResult::Consumed);
521 assert_eq!(view.selected(), 0);
522 }
523
524 #[test]
525 fn test_list_view_go_to_end() {
526 let mut view = TestListView::new(10);
527 let mut ctx = make_context();
528
529 let result = view.handle_key(make_key_event(KeyCode::Char('G')), &mut ctx);
531 assert_eq!(result, EventResult::Consumed);
532 assert_eq!(view.selected(), 9);
533
534 let result = view.handle_key(make_key_event(KeyCode::Down), &mut ctx);
536 assert_eq!(result, EventResult::Consumed);
537 assert_eq!(view.selected(), 9);
538 }
539
540 #[test]
541 fn test_event_result_display() {
542 assert_eq!(format!("{}", EventResult::Consumed), "Consumed");
543 assert_eq!(format!("{}", EventResult::Ignored), "Ignored");
544 assert_eq!(format!("{}", EventResult::Exit), "Exit");
545 }
546
547 #[test]
548 fn test_shortcut_creation() {
549 let shortcut = Shortcut::new("Enter", "Select item");
550 assert_eq!(shortcut.key, "Enter");
551 assert_eq!(shortcut.description, "Select item");
552 assert!(!shortcut.primary);
553
554 let primary = Shortcut::primary("q", "Quit");
555 assert!(primary.primary);
556 }
557
558 #[test]
559 fn test_event_result_helpers() {
560 let result = EventResult::status("Test message");
561 assert_eq!(
562 result,
563 EventResult::StatusMessage("Test message".to_string())
564 );
565
566 let nav = EventResult::navigate(TabTarget::Components);
567 assert_eq!(nav, EventResult::NavigateTo(TabTarget::Components));
568 }
569}