use crossterm::event::{KeyEvent, MouseEvent};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventResult {
Consumed,
Ignored,
NavigateTo(TabTarget),
Exit,
ShowOverlay(OverlayKind),
StatusMessage(String),
}
impl EventResult {
pub fn status(msg: impl Into<String>) -> Self {
Self::StatusMessage(msg.into())
}
#[must_use]
pub const fn navigate(target: TabTarget) -> Self {
Self::NavigateTo(target)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TabTarget {
Summary,
Overview,
Tree,
Components,
Dependencies,
Licenses,
Vulnerabilities,
Quality,
Compliance,
SideBySide,
GraphChanges,
Source,
ComponentByName(String),
VulnerabilityById(String),
ComponentByLicense(String),
}
impl TabTarget {
#[must_use]
pub const fn to_tab_kind(&self) -> Option<super::app::TabKind> {
match self {
Self::Summary => Some(super::app::TabKind::Summary),
Self::Overview => Some(super::app::TabKind::Overview),
Self::Tree => Some(super::app::TabKind::Tree),
Self::Components | Self::ComponentByName(_) | Self::ComponentByLicense(_) => {
Some(super::app::TabKind::Components)
}
Self::Dependencies => Some(super::app::TabKind::Dependencies),
Self::Licenses => Some(super::app::TabKind::Licenses),
Self::Vulnerabilities | Self::VulnerabilityById(_) => {
Some(super::app::TabKind::Vulnerabilities)
}
Self::Quality => Some(super::app::TabKind::Quality),
Self::Compliance => Some(super::app::TabKind::Compliance),
Self::SideBySide => Some(super::app::TabKind::SideBySide),
Self::GraphChanges => Some(super::app::TabKind::GraphChanges),
Self::Source => Some(super::app::TabKind::Source),
}
}
#[must_use]
pub const fn from_tab_kind(kind: super::app::TabKind) -> Self {
match kind {
super::app::TabKind::Summary => Self::Summary,
super::app::TabKind::Overview => Self::Overview,
super::app::TabKind::Tree => Self::Tree,
super::app::TabKind::Components => Self::Components,
super::app::TabKind::Dependencies => Self::Dependencies,
super::app::TabKind::Licenses => Self::Licenses,
super::app::TabKind::Vulnerabilities => Self::Vulnerabilities,
super::app::TabKind::Quality => Self::Quality,
super::app::TabKind::Compliance => Self::Compliance,
super::app::TabKind::SideBySide => Self::SideBySide,
super::app::TabKind::GraphChanges => Self::GraphChanges,
super::app::TabKind::Source => Self::Source,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OverlayKind {
Help,
Export,
Legend,
Search,
Shortcuts,
}
#[derive(Debug, Clone)]
pub struct Shortcut {
pub key: String,
pub description: String,
pub primary: bool,
}
impl Shortcut {
pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
Self {
key: key.into(),
description: description.into(),
primary: false,
}
}
pub fn primary(key: impl Into<String>, description: impl Into<String>) -> Self {
Self {
key: key.into(),
description: description.into(),
primary: true,
}
}
}
pub struct ViewContext<'a> {
pub mode: ViewMode,
pub focused: bool,
pub width: u16,
pub height: u16,
pub tick: u64,
pub status_message: &'a mut Option<String>,
}
impl ViewContext<'_> {
pub fn set_status(&mut self, msg: impl Into<String>) {
*self.status_message = Some(msg.into());
}
pub fn clear_status(&mut self) {
*self.status_message = None;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewMode {
Diff,
View,
MultiDiff,
Timeline,
Matrix,
}
impl ViewMode {
#[must_use]
pub const fn from_app_mode(mode: super::app::AppMode) -> Self {
match mode {
super::app::AppMode::Diff => Self::Diff,
super::app::AppMode::View => Self::View,
super::app::AppMode::MultiDiff => Self::MultiDiff,
super::app::AppMode::Timeline => Self::Timeline,
super::app::AppMode::Matrix => Self::Matrix,
}
}
}
pub trait ViewState: Send {
fn handle_key(&mut self, key: KeyEvent, ctx: &mut ViewContext) -> EventResult;
fn handle_mouse(&mut self, _mouse: MouseEvent, _ctx: &mut ViewContext) -> EventResult {
EventResult::Ignored
}
fn title(&self) -> &str;
fn shortcuts(&self) -> Vec<Shortcut>;
fn on_enter(&mut self, _ctx: &mut ViewContext) {}
fn on_leave(&mut self, _ctx: &mut ViewContext) {}
fn on_tick(&mut self, _ctx: &mut ViewContext) {}
fn has_modal(&self) -> bool {
false
}
}
pub trait ListViewState: ViewState {
fn selected(&self) -> usize;
fn set_selected(&mut self, idx: usize);
fn total(&self) -> usize;
fn select_next(&mut self) {
let total = self.total();
let selected = self.selected();
if total > 0 && selected < total.saturating_sub(1) {
self.set_selected(selected + 1);
}
}
fn select_prev(&mut self) {
let selected = self.selected();
if selected > 0 {
self.set_selected(selected - 1);
}
}
fn page_down(&mut self) {
use super::constants::PAGE_SIZE;
let total = self.total();
let selected = self.selected();
if total > 0 {
self.set_selected((selected + PAGE_SIZE).min(total.saturating_sub(1)));
}
}
fn page_up(&mut self) {
use super::constants::PAGE_SIZE;
let selected = self.selected();
self.set_selected(selected.saturating_sub(PAGE_SIZE));
}
fn go_first(&mut self) {
self.set_selected(0);
}
fn go_last(&mut self) {
let total = self.total();
if total > 0 {
self.set_selected(total.saturating_sub(1));
}
}
fn handle_list_nav_key(&mut self, key: KeyEvent) -> EventResult {
use crossterm::event::KeyCode;
match key.code {
KeyCode::Down | KeyCode::Char('j') => {
self.select_next();
EventResult::Consumed
}
KeyCode::Up | KeyCode::Char('k') => {
self.select_prev();
EventResult::Consumed
}
KeyCode::Home | KeyCode::Char('g') => {
self.go_first();
EventResult::Consumed
}
KeyCode::End | KeyCode::Char('G') => {
self.go_last();
EventResult::Consumed
}
KeyCode::PageDown => {
self.page_down();
EventResult::Consumed
}
KeyCode::PageUp => {
self.page_up();
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}
}
impl fmt::Display for EventResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Consumed => write!(f, "Consumed"),
Self::Ignored => write!(f, "Ignored"),
Self::NavigateTo(target) => write!(f, "NavigateTo({target:?})"),
Self::Exit => write!(f, "Exit"),
Self::ShowOverlay(kind) => write!(f, "ShowOverlay({kind:?})"),
Self::StatusMessage(msg) => write!(f, "StatusMessage({msg})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyModifiers};
struct TestListView {
selected: usize,
total: usize,
}
impl TestListView {
fn new(total: usize) -> Self {
Self { selected: 0, total }
}
}
impl ViewState for TestListView {
fn handle_key(&mut self, key: KeyEvent, _ctx: &mut ViewContext) -> EventResult {
self.handle_list_nav_key(key)
}
fn title(&self) -> &str {
"Test View"
}
fn shortcuts(&self) -> Vec<Shortcut> {
vec![
Shortcut::primary("j/k", "Navigate"),
Shortcut::new("g/G", "First/Last"),
]
}
}
impl ListViewState for TestListView {
fn selected(&self) -> usize {
self.selected
}
fn set_selected(&mut self, idx: usize) {
self.selected = idx;
}
fn total(&self) -> usize {
self.total
}
}
fn make_key_event(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::empty())
}
fn make_context() -> ViewContext<'static> {
let status: &'static mut Option<String> = Box::leak(Box::new(None));
ViewContext {
mode: ViewMode::Diff,
focused: true,
width: 80,
height: 24,
tick: 0,
status_message: status,
}
}
#[test]
fn test_list_view_navigation() {
let mut view = TestListView::new(10);
let mut ctx = make_context();
assert_eq!(view.selected(), 0);
let result = view.handle_key(make_key_event(KeyCode::Down), &mut ctx);
assert_eq!(result, EventResult::Consumed);
assert_eq!(view.selected(), 1);
let result = view.handle_key(make_key_event(KeyCode::Up), &mut ctx);
assert_eq!(result, EventResult::Consumed);
assert_eq!(view.selected(), 0);
let result = view.handle_key(make_key_event(KeyCode::Up), &mut ctx);
assert_eq!(result, EventResult::Consumed);
assert_eq!(view.selected(), 0);
}
#[test]
fn test_list_view_go_to_end() {
let mut view = TestListView::new(10);
let mut ctx = make_context();
let result = view.handle_key(make_key_event(KeyCode::Char('G')), &mut ctx);
assert_eq!(result, EventResult::Consumed);
assert_eq!(view.selected(), 9);
let result = view.handle_key(make_key_event(KeyCode::Down), &mut ctx);
assert_eq!(result, EventResult::Consumed);
assert_eq!(view.selected(), 9);
}
#[test]
fn test_event_result_display() {
assert_eq!(format!("{}", EventResult::Consumed), "Consumed");
assert_eq!(format!("{}", EventResult::Ignored), "Ignored");
assert_eq!(format!("{}", EventResult::Exit), "Exit");
}
#[test]
fn test_shortcut_creation() {
let shortcut = Shortcut::new("Enter", "Select item");
assert_eq!(shortcut.key, "Enter");
assert_eq!(shortcut.description, "Select item");
assert!(!shortcut.primary);
let primary = Shortcut::primary("q", "Quit");
assert!(primary.primary);
}
#[test]
fn test_event_result_helpers() {
let result = EventResult::status("Test message");
assert_eq!(
result,
EventResult::StatusMessage("Test message".to_string())
);
let nav = EventResult::navigate(TabTarget::Components);
assert_eq!(nav, EventResult::NavigateTo(TabTarget::Components));
}
}