use crate::tui::app_states::dependencies::DependenciesState;
use crate::tui::state::ListNavigation;
use crate::tui::traits::{EventResult, Shortcut, ViewContext, ViewMode, ViewState};
use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
pub struct DependenciesView {
inner: DependenciesState,
}
impl DependenciesView {
pub(crate) fn new() -> Self {
Self {
inner: DependenciesState::new(),
}
}
pub(crate) fn inner(&self) -> &DependenciesState {
&self.inner
}
pub(crate) fn inner_mut(&mut self) -> &mut DependenciesState {
&mut self.inner
}
}
impl Default for DependenciesView {
fn default() -> Self {
Self::new()
}
}
impl ViewState for DependenciesView {
fn handle_key(&mut self, key: KeyEvent, ctx: &mut ViewContext) -> EventResult {
if self.inner.is_searching() {
return self.handle_search_key(key);
}
if self.inner.has_search_query() {
match key.code {
KeyCode::Esc => {
self.inner.clear_search();
return EventResult::Consumed;
}
KeyCode::Char('n') => {
self.inner.next_match();
return EventResult::Consumed;
}
KeyCode::Char('N') => {
self.inner.prev_match();
return EventResult::Consumed;
}
KeyCode::Char('/') => {
self.inner.search_active = true;
return EventResult::Consumed;
}
_ => {} }
}
if self.inner.show_deps_help {
if matches!(key.code, KeyCode::Esc | KeyCode::Char('?' | 'q')) {
self.inner.show_deps_help = false;
}
return EventResult::Consumed;
}
match key.code {
KeyCode::Char('/') => {
self.inner.start_search();
EventResult::Consumed
}
KeyCode::Char('?') => {
self.inner.toggle_deps_help();
EventResult::Consumed
}
KeyCode::Char('t') => {
self.inner.toggle_transitive();
EventResult::Consumed
}
KeyCode::Char('h') => {
if ctx.mode == ViewMode::Diff {
self.inner.toggle_highlight();
}
EventResult::Consumed
}
KeyCode::Char('y') => {
self.inner.toggle_cycles();
EventResult::Consumed
}
KeyCode::Char('f') => {
self.inner.toggle_change_filter();
let label = self.inner.change_filter.label();
EventResult::status(format!("Dependency filter: {label}"))
}
KeyCode::Char('s') => {
self.inner.toggle_sort();
EventResult::Consumed
}
KeyCode::Char('e') => {
self.inner.expand_all();
EventResult::Consumed
}
KeyCode::Char('E') => {
self.inner.collapse_all();
EventResult::Consumed
}
KeyCode::Char('b') => {
self.inner.toggle_breadcrumbs();
EventResult::Consumed
}
KeyCode::Char('+' | '=') => {
self.inner.increase_depth();
EventResult::Consumed
}
KeyCode::Char('-' | '_') => {
self.inner.decrease_depth();
EventResult::Consumed
}
KeyCode::Char('>' | '.') => {
self.inner.increase_roots();
EventResult::Consumed
}
KeyCode::Char('<' | ',') => {
self.inner.decrease_roots();
EventResult::Consumed
}
KeyCode::Up | KeyCode::Char('k') => {
self.inner.select_prev();
self.inner.adjust_scroll_to_selection();
self.inner.update_breadcrumbs();
self.inner.detail_scroll = 0;
EventResult::Consumed
}
KeyCode::Down | KeyCode::Char('j') => {
self.inner.select_next();
self.inner.adjust_scroll_to_selection();
self.inner.update_breadcrumbs();
self.inner.detail_scroll = 0;
EventResult::Consumed
}
KeyCode::Char('d')
if key
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL) =>
{
self.inner.detail_scroll = self.inner.detail_scroll.saturating_add(3);
EventResult::Consumed
}
KeyCode::Char('u')
if key
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL) =>
{
self.inner.detail_scroll = self.inner.detail_scroll.saturating_sub(3);
EventResult::Consumed
}
KeyCode::Enter => {
if let Some(node_id) = self.inner.get_selected_node_id().map(str::to_string) {
self.inner.toggle_node(&node_id);
}
EventResult::Consumed
}
KeyCode::Char('c') => EventResult::Ignored,
KeyCode::Left => {
if let Some(node_id) = self.inner.get_selected_node_id().map(str::to_string) {
self.inner.collapse(&node_id);
}
EventResult::Consumed
}
KeyCode::Right => {
if let Some(node_id) = self.inner.get_selected_node_id().map(str::to_string) {
self.inner.expand(&node_id);
}
EventResult::Consumed
}
KeyCode::Home => {
self.inner.selected = 0;
self.inner.adjust_scroll_to_selection();
self.inner.update_breadcrumbs();
self.inner.detail_scroll = 0;
EventResult::Consumed
}
KeyCode::End | KeyCode::Char('G') => {
if self.inner.total > 0 {
self.inner.selected = self.inner.total - 1;
self.inner.adjust_scroll_to_selection();
self.inner.update_breadcrumbs();
self.inner.detail_scroll = 0;
}
EventResult::Consumed
}
KeyCode::PageUp => {
let jump = self.inner.viewport_height.saturating_sub(2);
self.inner.selected = self.inner.selected.saturating_sub(jump);
self.inner.adjust_scroll_to_selection();
self.inner.update_breadcrumbs();
self.inner.detail_scroll = 0;
EventResult::Consumed
}
KeyCode::PageDown => {
let jump = self.inner.viewport_height.saturating_sub(2);
let new_sel = self.inner.selected + jump;
if self.inner.total > 0 {
self.inner.selected = new_sel.min(self.inner.total - 1);
self.inner.adjust_scroll_to_selection();
self.inner.update_breadcrumbs();
self.inner.detail_scroll = 0;
}
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}
fn handle_mouse(&mut self, _mouse: MouseEvent, _ctx: &mut ViewContext) -> EventResult {
EventResult::Ignored
}
fn title(&self) -> &'static str {
"Dependencies"
}
fn shortcuts(&self) -> Vec<Shortcut> {
vec![
Shortcut::primary("j/k", "Navigate"),
Shortcut::new("t", "Transitive"),
Shortcut::new("+/-", "Depth"),
Shortcut::new(">/<", "Roots"),
Shortcut::new("e/E", "Expand/Collapse all"),
Shortcut::new("/", "Search"),
Shortcut::new("s", "Sort"),
Shortcut::new("?", "Help"),
]
}
}
impl DependenciesView {
fn handle_search_key(&mut self, key: KeyEvent) -> EventResult {
match key.code {
KeyCode::Esc => {
self.inner.stop_search();
EventResult::Consumed
}
KeyCode::Enter => {
self.inner.stop_search();
EventResult::Consumed
}
KeyCode::Backspace => {
self.inner.search_pop();
EventResult::Consumed
}
KeyCode::Char('f')
if key
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL) =>
{
self.inner.toggle_filter_mode();
EventResult::Consumed
}
KeyCode::Char('f') => {
self.inner.toggle_filter_mode();
EventResult::Consumed
}
KeyCode::Char('n') => {
self.inner.next_match();
EventResult::Consumed
}
KeyCode::Char('N') => {
self.inner.prev_match();
EventResult::Consumed
}
KeyCode::Char(c) => {
self.inner.search_push(c);
EventResult::Consumed
}
_ => EventResult::Consumed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::traits::ViewMode;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn make_key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn make_ctx() -> 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_transitive_toggle() {
let mut view = DependenciesView::new();
let mut ctx = make_ctx();
let initial = view.inner().show_transitive;
view.handle_key(make_key(KeyCode::Char('t')), &mut ctx);
assert_ne!(view.inner().show_transitive, initial);
}
#[test]
fn test_depth_controls() {
let mut view = DependenciesView::new();
let mut ctx = make_ctx();
let initial_depth = view.inner().max_depth;
view.handle_key(make_key(KeyCode::Char('+')), &mut ctx);
assert!(view.inner().max_depth > initial_depth);
view.handle_key(make_key(KeyCode::Char('-')), &mut ctx);
assert_eq!(view.inner().max_depth, initial_depth);
}
#[test]
fn test_search_mode() {
let mut view = DependenciesView::new();
let mut ctx = make_ctx();
assert!(!view.inner().search_active);
view.handle_key(make_key(KeyCode::Char('/')), &mut ctx);
assert!(view.inner().search_active);
view.handle_key(make_key(KeyCode::Esc), &mut ctx);
assert!(!view.inner().search_active);
}
#[test]
fn test_cross_tab_nav_ignored() {
let mut view = DependenciesView::new();
let mut ctx = make_ctx();
let result = view.handle_key(make_key(KeyCode::Char('c')), &mut ctx);
assert_eq!(result, EventResult::Ignored);
}
}