use crate::tui::state::{ListNavigation, TreeNavigation};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum DependencyChangeFilter {
#[default]
All,
Added,
Removed,
}
impl DependencyChangeFilter {
pub const fn next(self) -> Self {
match self {
Self::All => Self::Added,
Self::Added => Self::Removed,
Self::Removed => Self::All,
}
}
pub const fn label(self) -> &'static str {
match self {
Self::All => "All",
Self::Added => "Added",
Self::Removed => "Removed",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum DependencySort {
#[default]
Name,
Depth,
VulnCount,
DependentCount,
}
impl DependencySort {
pub const fn next(self) -> Self {
match self {
Self::Name => Self::Depth,
Self::Depth => Self::VulnCount,
Self::VulnCount => Self::DependentCount,
Self::DependentCount => Self::Name,
}
}
pub const fn display_name(&self) -> &str {
match self {
Self::Name => "Name",
Self::Depth => "Depth",
Self::VulnCount => "Vulnerabilities",
Self::DependentCount => "Dependents",
}
}
}
pub struct DependenciesState {
pub show_transitive: bool,
pub highlight_changes: bool,
pub expanded_nodes: HashSet<String>,
pub selected: usize,
pub total: usize,
pub visible_nodes: Vec<String>,
pub max_depth: usize,
pub max_roots: usize,
pub show_cycles: bool,
pub detected_cycles: Vec<Vec<String>>,
pub graph_hash: u64,
pub search_active: bool,
pub search_query: String,
pub search_matches: HashSet<String>,
pub filter_mode: bool,
pub cached_graph: HashMap<String, Vec<String>>,
pub cached_roots: Vec<String>,
pub cached_vuln_components: HashSet<String>,
pub cache_valid: bool,
pub scroll_offset: usize,
pub viewport_height: usize,
pub breadcrumb_trail: Vec<String>,
pub show_breadcrumbs: bool,
pub show_deps_help: bool,
pub cached_direct_deps: HashSet<String>,
pub cached_reverse_graph: HashMap<String, Vec<String>>,
pub cached_forward_graph: HashMap<String, Vec<String>>,
pub sort_order: DependencySort,
pub change_filter: DependencyChangeFilter,
pub cached_depths: HashMap<String, usize>,
pub cached_display_names: HashMap<String, String>,
pub cached_edge_info: HashMap<(String, String), EdgeInfo>,
pub detail_scroll: usize,
}
#[derive(Debug, Clone)]
pub struct EdgeInfo {
pub relationship: String,
pub scope: Option<String>,
}
impl DependenciesState {
pub fn new() -> Self {
Self {
show_transitive: false,
highlight_changes: true,
expanded_nodes: HashSet::new(),
selected: 0,
total: 0,
visible_nodes: Vec::new(),
max_depth: crate::tui::constants::DEFAULT_TREE_MAX_DEPTH,
max_roots: crate::tui::constants::DEFAULT_TREE_MAX_ROOTS,
show_cycles: true,
detected_cycles: Vec::new(),
graph_hash: 0,
search_active: false,
search_query: String::new(),
search_matches: HashSet::new(),
filter_mode: false,
cached_graph: HashMap::new(),
cached_roots: Vec::new(),
cached_vuln_components: HashSet::new(),
cache_valid: false,
scroll_offset: 0,
viewport_height: 0,
breadcrumb_trail: Vec::new(),
show_breadcrumbs: true,
show_deps_help: false,
cached_direct_deps: HashSet::new(),
cached_reverse_graph: HashMap::new(),
cached_forward_graph: HashMap::new(),
sort_order: DependencySort::default(),
change_filter: DependencyChangeFilter::default(),
cached_depths: HashMap::new(),
cached_display_names: HashMap::new(),
cached_edge_info: HashMap::new(),
detail_scroll: 0,
}
}
pub const fn needs_cache_refresh(&self, new_hash: u64) -> bool {
!self.cache_valid || self.graph_hash != new_hash
}
pub fn update_graph_cache(
&mut self,
graph: HashMap<String, Vec<String>>,
roots: Vec<String>,
hash: u64,
) {
self.cached_graph = graph;
self.cached_roots = roots;
self.graph_hash = hash;
self.cache_valid = true;
}
pub fn update_vuln_cache(&mut self, vuln_components: HashSet<String>) {
self.cached_vuln_components = vuln_components;
}
pub fn adjust_scroll_to_selection(&mut self) {
if self.viewport_height == 0 {
return;
}
let padding = 2.min(self.viewport_height.saturating_sub(1)); if self.selected < self.scroll_offset.saturating_add(padding) {
self.scroll_offset = self.selected.saturating_sub(padding);
} else if self.selected
>= self
.scroll_offset
.saturating_add(self.viewport_height.saturating_sub(padding))
{
self.scroll_offset = self.selected.saturating_sub(
self.viewport_height
.saturating_sub(padding)
.saturating_sub(1),
);
}
}
pub const fn increase_depth(&mut self) {
if self.max_depth < crate::tui::constants::MAX_TREE_DEPTH {
self.max_depth += 1;
}
}
pub const fn decrease_depth(&mut self) {
if self.max_depth > 1 {
self.max_depth -= 1;
}
}
pub const fn increase_roots(&mut self) {
use crate::tui::constants::{MAX_TREE_ROOTS, TREE_ROOTS_STEP};
if self.max_roots < MAX_TREE_ROOTS {
self.max_roots += TREE_ROOTS_STEP;
}
}
pub const fn decrease_roots(&mut self) {
use crate::tui::constants::{MIN_TREE_ROOTS, TREE_ROOTS_STEP};
if self.max_roots > MIN_TREE_ROOTS {
self.max_roots -= TREE_ROOTS_STEP;
}
}
pub const fn toggle_cycles(&mut self) {
self.show_cycles = !self.show_cycles;
}
pub const fn toggle_transitive(&mut self) {
self.show_transitive = !self.show_transitive;
}
pub const fn toggle_highlight(&mut self) {
self.highlight_changes = !self.highlight_changes;
}
pub const fn toggle_sort(&mut self) {
self.sort_order = self.sort_order.next();
}
pub fn sort_roots(&self, roots: &mut [String]) {
match self.sort_order {
DependencySort::Name => {} DependencySort::Depth => {
roots.sort_by_key(|id| self.cached_depths.get(id).copied().unwrap_or(0));
}
DependencySort::VulnCount => {
roots.sort_by(|a, b| {
let va = self.cached_vuln_components.contains(a);
let vb = self.cached_vuln_components.contains(b);
vb.cmp(&va).then_with(|| a.cmp(b))
});
}
DependencySort::DependentCount => {
roots.sort_by(|a, b| {
let da = self.cached_reverse_graph.get(a).map_or(0, Vec::len);
let db = self.cached_reverse_graph.get(b).map_or(0, Vec::len);
db.cmp(&da).then_with(|| a.cmp(b))
});
}
}
}
pub const fn toggle_change_filter(&mut self) {
self.change_filter = self.change_filter.next();
}
pub fn update_transitive_cache(&mut self) {
self.cached_direct_deps.clear();
self.cached_reverse_graph.clear();
self.cached_forward_graph.clear();
self.cached_depths.clear();
for (source, targets) in &self.cached_graph {
if !self.cached_depths.contains_key(source) && self.cached_roots.contains(source) {
self.cached_depths.insert(source.clone(), 0);
}
for target in targets {
self.cached_reverse_graph
.entry(target.clone())
.or_default()
.push(source.clone());
if self.cached_roots.contains(source) {
self.cached_direct_deps.insert(target.clone());
}
}
}
let mut queue: std::collections::VecDeque<(String, usize)> =
self.cached_roots.iter().map(|r| (r.clone(), 0)).collect();
while let Some((node, depth)) = queue.pop_front() {
if let Some(&existing_depth) = self.cached_depths.get(node.as_str())
&& existing_depth <= depth
{
continue; }
if let Some(children) = self.cached_graph.get(node.as_str()) {
for child in children {
let dominated = self
.cached_depths
.get(child.as_str())
.is_none_or(|&d| d > depth + 1);
if dominated {
queue.push_back((child.clone(), depth + 1));
}
}
}
self.cached_depths.insert(node, depth);
}
for (child, parents) in &self.cached_reverse_graph {
for parent in parents {
self.cached_forward_graph
.entry(parent.clone())
.or_default()
.push(child.clone());
}
}
}
pub fn toggle_node(&mut self, node_id: &str) {
if self.expanded_nodes.contains(node_id) {
self.expanded_nodes.remove(node_id);
} else {
self.expanded_nodes.insert(node_id.to_string());
}
}
pub fn expand(&mut self, node_id: &str) {
self.expanded_nodes.insert(node_id.to_string());
}
pub fn collapse(&mut self, node_id: &str) {
self.expanded_nodes.remove(node_id);
}
pub fn get_selected_node_id(&self) -> Option<&str> {
self.visible_nodes
.get(self.selected)
.map(std::string::String::as_str)
}
pub fn start_search(&mut self) {
self.search_active = true;
self.search_query.clear();
self.search_matches.clear();
}
pub const fn stop_search(&mut self) {
self.search_active = false;
}
pub fn clear_search(&mut self) {
self.search_active = false;
self.search_query.clear();
self.search_matches.clear();
self.filter_mode = false;
}
pub const fn is_searching(&self) -> bool {
self.search_active
}
pub fn has_search_query(&self) -> bool {
!self.search_query.is_empty()
}
pub const fn toggle_filter_mode(&mut self) {
self.filter_mode = !self.filter_mode;
}
pub fn update_search_matches(&mut self, all_node_names: &[(String, String)]) {
self.search_matches.clear();
if self.search_query.is_empty() {
return;
}
let query_lower = self.search_query.to_lowercase();
for (node_id, node_name) in all_node_names {
if node_name.to_lowercase().contains(&query_lower) {
self.search_matches.insert(node_id.clone());
} else if let Some(display_name) = self.cached_display_names.get(node_id)
&& display_name.to_lowercase().contains(&query_lower)
{
self.search_matches.insert(node_id.clone());
}
}
}
pub fn search_push(&mut self, c: char) {
self.search_query.push(c);
}
pub fn search_pop(&mut self) {
self.search_query.pop();
}
pub fn next_match(&mut self) {
if self.search_matches.is_empty() || self.visible_nodes.is_empty() {
return;
}
let len = self.visible_nodes.len();
let sel = self.selected.min(len.saturating_sub(1));
for i in (sel + 1)..len {
if self.search_matches.contains(&self.visible_nodes[i]) {
self.selected = i;
return;
}
}
for i in 0..=sel {
if self.search_matches.contains(&self.visible_nodes[i]) {
self.selected = i;
return;
}
}
}
pub fn prev_match(&mut self) {
if self.search_matches.is_empty() || self.visible_nodes.is_empty() {
return;
}
let len = self.visible_nodes.len();
let sel = self.selected.min(len.saturating_sub(1));
for i in (0..sel).rev() {
if self.search_matches.contains(&self.visible_nodes[i]) {
self.selected = i;
return;
}
}
for i in (sel..len).rev() {
if self.search_matches.contains(&self.visible_nodes[i]) {
self.selected = i;
return;
}
}
}
pub fn expand_all(&mut self) {
for root in &self.cached_roots {
self.expanded_nodes.insert(root.clone());
}
for (node, children) in &self.cached_graph {
if !children.is_empty() {
self.expanded_nodes.insert(node.clone());
}
}
}
pub fn collapse_all(&mut self) {
self.expanded_nodes.clear();
}
pub const fn toggle_breadcrumbs(&mut self) {
self.show_breadcrumbs = !self.show_breadcrumbs;
}
pub const fn toggle_deps_help(&mut self) {
self.show_deps_help = !self.show_deps_help;
}
pub fn update_breadcrumbs(&mut self) {
self.breadcrumb_trail.clear();
let Some(selected_id) = self.visible_nodes.get(self.selected) else {
return;
};
if selected_id.starts_with("__") {
return;
}
let parts: Vec<&str> = selected_id.split(':').collect();
if parts.len() == 1 {
self.breadcrumb_trail.push(parts[0].to_string());
} else {
for part in &parts {
if *part == "+" || *part == "-" {
continue; }
self.breadcrumb_trail.push(part.to_string());
}
}
}
pub fn get_breadcrumb_display(&self) -> String {
if self.breadcrumb_trail.is_empty() {
return String::new();
}
self.breadcrumb_trail
.iter()
.map(|id| {
self.cached_display_names
.get(id)
.map_or(id.as_str(), String::as_str)
})
.collect::<Vec<_>>()
.join(" → ")
}
}
impl ListNavigation for DependenciesState {
fn selected(&self) -> usize {
self.selected
}
fn set_selected(&mut self, idx: usize) {
self.selected = idx;
}
fn total(&self) -> usize {
self.total
}
fn set_total(&mut self, total: usize) {
self.total = total;
self.clamp_selection();
}
}
impl TreeNavigation for DependenciesState {
fn is_expanded(&self, node_id: &str) -> bool {
self.expanded_nodes.contains(node_id)
}
fn expand(&mut self, node_id: &str) {
self.expanded_nodes.insert(node_id.to_string());
}
fn collapse(&mut self, node_id: &str) {
self.expanded_nodes.remove(node_id);
}
fn expand_all(&mut self) {
for root in &self.cached_roots {
self.expanded_nodes.insert(root.clone());
}
for (node, children) in &self.cached_graph {
if !children.is_empty() {
self.expanded_nodes.insert(node.clone());
}
}
}
fn collapse_all(&mut self) {
self.expanded_nodes.clear();
}
}
impl Default for DependenciesState {
fn default() -> Self {
Self::new()
}
}