use crate::tui::App;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::cell::Cell;
thread_local! {
static PAIR_DIFF_SCROLL: Cell<usize> = const { Cell::new(0) };
}
pub(crate) fn pair_diff_scroll() -> usize {
PAIR_DIFF_SCROLL.with(Cell::get)
}
pub(crate) fn set_pair_diff_scroll(offset: usize) {
PAIR_DIFF_SCROLL.with(|c| c.set(offset));
}
pub(super) fn handle_matrix_keys(app: &mut App, key: KeyEvent) -> bool {
if app.tabs.matrix.search.active {
handle_matrix_search(app, key);
return true;
}
if app.tabs.matrix.show_pair_diff {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.tabs.matrix.close_pair_diff();
set_pair_diff_scroll(0);
}
KeyCode::Down | KeyCode::Char('j') => {
set_pair_diff_scroll(pair_diff_scroll().saturating_add(1));
}
KeyCode::Up | KeyCode::Char('k') => {
set_pair_diff_scroll(pair_diff_scroll().saturating_sub(1));
}
_ => {}
}
return true;
}
if app.tabs.matrix.show_export_options {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.tabs.matrix.close_export_options();
}
KeyCode::Char('c') => {
app.tabs.matrix.close_export_options();
app.export_matrix(crate::tui::export::ExportFormat::Csv);
}
KeyCode::Char('j') => {
app.tabs.matrix.close_export_options();
app.export_matrix(crate::tui::export::ExportFormat::Json);
}
KeyCode::Char('h') => {
app.tabs.matrix.close_export_options();
app.export_matrix(crate::tui::export::ExportFormat::Html);
}
_ => {}
}
return true;
}
if app.tabs.matrix.show_clustering_details {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.tabs.matrix.close_clustering_details();
}
KeyCode::Up | KeyCode::Char('k') => {
app.tabs.matrix.select_prev_cluster();
}
KeyCode::Down | KeyCode::Char('j') => {
app.tabs.matrix.select_next_cluster();
}
_ => {}
}
return true;
}
match key.code {
KeyCode::Tab | KeyCode::BackTab | KeyCode::Char('p') => app.tabs.matrix.toggle_panel(),
KeyCode::Up | KeyCode::Char('k') => app.tabs.matrix.move_up(),
KeyCode::Down | KeyCode::Char('j') => app.tabs.matrix.move_down(),
KeyCode::Left | KeyCode::Char('h') => app.tabs.matrix.move_left(),
KeyCode::Right | KeyCode::Char('l') => app.tabs.matrix.move_right(),
KeyCode::Char('/') => {
app.tabs.matrix.search.start();
}
KeyCode::Char('n') if !app.tabs.matrix.search.matches.is_empty() => {
app.tabs.matrix.search.next_match();
if let Some(idx) = app.tabs.matrix.search.current_match_index() {
app.tabs.matrix.selected_row = idx;
}
}
KeyCode::Char('N') if !app.tabs.matrix.search.matches.is_empty() => {
app.tabs.matrix.search.prev_match();
if let Some(idx) = app.tabs.matrix.search.current_match_index() {
app.tabs.matrix.selected_row = idx;
}
}
KeyCode::Char('s') => {
app.tabs.matrix.toggle_sort();
app.tabs.matrix.clear_focus();
update_matrix_search_matches(app);
app.set_status_message(format!(
"Sort: {} {}",
app.tabs.matrix.sort_by.label(),
app.tabs.matrix.sort_direction.indicator()
));
}
KeyCode::Char('S') => {
app.tabs.matrix.toggle_sort_direction();
app.tabs.matrix.clear_focus();
update_matrix_search_matches(app);
}
KeyCode::Char('t') => {
app.tabs.matrix.toggle_threshold();
app.set_status_message(format!("Threshold: {}", app.tabs.matrix.threshold.label()));
}
KeyCode::Char('z') => {
app.tabs.matrix.toggle_focus_mode();
let status = if app.tabs.matrix.focus_mode {
"Focus mode: enabled"
} else {
"Focus mode: disabled"
};
app.set_status_message(status.to_string());
}
KeyCode::Char('r') => {
app.tabs.matrix.focus_on_row(app.tabs.matrix.selected_row);
app.set_status_message(format!(
"Focused on row {}",
app.tabs.matrix.selected_row + 1
));
}
KeyCode::Char('c') => {
app.tabs.matrix.focus_on_col(app.tabs.matrix.selected_col);
app.set_status_message(format!(
"Focused on column {}",
app.tabs.matrix.selected_col + 1
));
}
KeyCode::Esc => {
if app.tabs.matrix.focus_mode {
app.tabs.matrix.clear_focus();
app.set_status_message("Focus cleared".to_string());
}
}
KeyCode::Char('H') => {
app.tabs.matrix.toggle_row_col_highlight();
let status = if app.tabs.matrix.highlight_row_col {
"Row/column highlight: enabled"
} else {
"Row/column highlight: disabled"
};
app.set_status_message(status.to_string());
}
KeyCode::Enter | KeyCode::Char('d') => {
if app.tabs.matrix.selected_row == app.tabs.matrix.selected_col {
app.set_status_message("Cannot diff same SBOM".to_string());
} else {
app.tabs.matrix.toggle_pair_diff();
set_pair_diff_scroll(0);
}
}
KeyCode::Char('x') => {
app.tabs.matrix.toggle_export_options();
}
KeyCode::Char('D') => {
app.set_status_message(
"Deep dive applies to components — press Enter for pair diff".to_string(),
);
}
KeyCode::Char('C') => {
app.tabs.matrix.toggle_clustering_details();
}
_ => return false,
}
true
}
pub(super) fn handle_matrix_search(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Esc => {
app.tabs.matrix.search.cancel();
}
KeyCode::Enter => {
app.tabs.matrix.search.confirm();
if let Some(idx) = app.tabs.matrix.search.current_match_index() {
app.tabs.matrix.selected_row = idx;
}
}
KeyCode::Backspace => {
app.tabs.matrix.search.pop();
update_matrix_search_matches(app);
}
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.tabs.matrix.search.toggle_mode();
update_matrix_search_matches(app);
app.set_status_message(format!(
"Search mode: {}",
app.tabs.matrix.search.mode.label()
));
}
KeyCode::Down => {
app.tabs.matrix.search.next_match();
if let Some(idx) = app.tabs.matrix.search.current_match_index() {
app.tabs.matrix.selected_row = idx;
}
}
KeyCode::Up => {
app.tabs.matrix.search.prev_match();
if let Some(idx) = app.tabs.matrix.search.current_match_index() {
app.tabs.matrix.selected_row = idx;
}
}
KeyCode::Char(c) => {
app.tabs.matrix.search.push(c);
update_matrix_search_matches(app);
}
_ => {}
}
}
pub(super) fn update_matrix_search_matches(app: &mut App) {
let query = &app.tabs.matrix.search.query;
if query.is_empty() {
app.tabs.matrix.search.error = None;
app.tabs.matrix.search.update_matches(vec![]);
return;
}
let matcher =
match crate::tui::app_states::SearchMatcher::build(query, app.tabs.matrix.search.mode) {
Ok(m) => {
app.tabs.matrix.search.error = None;
m
}
Err(e) => {
app.tabs.matrix.search.error = Some(e);
app.tabs.matrix.search.update_matches(vec![]);
return;
}
};
let matches: Vec<usize> = app
.data
.matrix_result
.as_ref()
.map_or_else(Vec::new, |result| {
crate::tui::views::ordered_sbom_indices(result, &app.tabs.matrix)
.iter()
.enumerate()
.filter(|(_, raw)| matcher.is_match(&result.sboms[**raw].name))
.map(|(display, _)| display)
.collect()
});
app.tabs.matrix.search.update_matches(matches);
}