1mod components;
9mod compliance;
10mod dependencies;
11mod graph_changes;
12mod helpers;
13mod licenses;
14mod matrix;
15pub mod mouse;
16mod multi_diff;
17mod quality;
18mod sidebyside;
19mod source;
20mod timeline;
21mod vulnerabilities;
22
23use crate::config::TuiPreferences;
24use crate::tui::toggle_theme;
25use crossterm::event::{
26 self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent,
27};
28use std::time::Duration;
29
30pub use mouse::handle_mouse_event;
31
32#[derive(Debug)]
34pub enum Event {
35 Key(KeyEvent),
37 Mouse(MouseEvent),
39 Tick,
41 Resize(u16, u16),
43}
44
45pub struct EventHandler {
47 tick_rate: Duration,
49}
50
51impl EventHandler {
52 pub fn new(tick_rate: u64) -> Self {
54 Self {
55 tick_rate: Duration::from_millis(tick_rate),
56 }
57 }
58
59 pub fn next(&self) -> Result<Event, std::io::Error> {
61 if event::poll(self.tick_rate)? {
62 match event::read()? {
63 CrosstermEvent::Key(key) => Ok(Event::Key(key)),
64 CrosstermEvent::Mouse(mouse) => Ok(Event::Mouse(mouse)),
65 CrosstermEvent::Resize(width, height) => Ok(Event::Resize(width, height)),
66 _ => Ok(Event::Tick),
67 }
68 } else {
69 Ok(Event::Tick)
70 }
71 }
72}
73
74impl Default for EventHandler {
75 fn default() -> Self {
76 Self::new(250)
77 }
78}
79
80pub fn handle_key_event(app: &mut super::App, key: KeyEvent) {
82 app.clear_status_message();
84
85 if app.overlays.search.active {
87 match key.code {
88 KeyCode::Esc => app.stop_search(),
89 KeyCode::Enter => {
90 app.jump_to_search_result();
92 }
93 KeyCode::Backspace => {
94 app.search_pop();
95 app.execute_search();
97 }
98 KeyCode::Up => app.overlays.search.select_prev(),
99 KeyCode::Down => app.overlays.search.select_next(),
100 KeyCode::Char(c) => {
101 app.search_push(c);
102 app.execute_search();
104 }
105 _ => {}
106 }
107 return;
108 }
109
110 if app.overlays.threshold_tuning.visible {
112 match key.code {
113 KeyCode::Esc | KeyCode::Char('q') => {
114 app.overlays.threshold_tuning.visible = false;
115 }
116 KeyCode::Up | KeyCode::Char('k') => {
117 app.overlays.threshold_tuning.increase();
118 app.update_threshold_preview();
119 }
120 KeyCode::Down | KeyCode::Char('j') => {
121 app.overlays.threshold_tuning.decrease();
122 app.update_threshold_preview();
123 }
124 KeyCode::Right | KeyCode::Char('l') => {
125 app.overlays.threshold_tuning.fine_increase();
126 app.update_threshold_preview();
127 }
128 KeyCode::Left | KeyCode::Char('h') => {
129 app.overlays.threshold_tuning.fine_decrease();
130 app.update_threshold_preview();
131 }
132 KeyCode::Char('r') => {
133 app.overlays.threshold_tuning.reset();
134 app.update_threshold_preview();
135 }
136 KeyCode::Enter => {
137 app.apply_threshold();
138 }
139 _ => {}
140 }
141 return;
142 }
143
144 if app.has_overlay() {
146 match key.code {
147 KeyCode::Esc => app.close_overlays(),
148 KeyCode::Char('?') if app.overlays.show_help => app.toggle_help(),
149 KeyCode::Char('e') if app.overlays.show_export => app.toggle_export(),
150 KeyCode::Char('l') if app.overlays.show_legend => app.toggle_legend(),
151 KeyCode::Char('q') => app.close_overlays(),
152 KeyCode::Char('j') if app.overlays.show_export => {
154 app.close_overlays();
155 app.export(super::export::ExportFormat::Json);
156 }
157 KeyCode::Char('m') if app.overlays.show_export => {
158 app.close_overlays();
159 app.export(super::export::ExportFormat::Markdown);
160 }
161 KeyCode::Char('h') if app.overlays.show_export => {
162 app.close_overlays();
163 app.export(super::export::ExportFormat::Html);
164 }
165 KeyCode::Char('s') if app.overlays.show_export => {
166 app.close_overlays();
167 app.export(super::export::ExportFormat::Sarif);
168 }
169 KeyCode::Char('d') | KeyCode::Char('c') if app.overlays.show_export => {
170 app.close_overlays();
171 app.export(super::export::ExportFormat::Csv);
172 }
173 _ => {}
174 }
175 return;
176 }
177
178 if app.overlays.view_switcher.visible {
180 match key.code {
181 KeyCode::Esc => app.overlays.view_switcher.hide(),
182 KeyCode::Up | KeyCode::Char('k') => app.overlays.view_switcher.previous(),
183 KeyCode::Down | KeyCode::Char('j') => app.overlays.view_switcher.next(),
184 KeyCode::Enter | KeyCode::Char(' ') => {
185 if let Some(view) = app.overlays.view_switcher.current_view() {
186 app.overlays.view_switcher.hide();
187 mouse::switch_to_view(app, view);
188 }
189 }
190 KeyCode::Char('1') => {
191 app.overlays.view_switcher.hide();
192 mouse::switch_to_view(app, super::app::MultiViewType::MultiDiff);
193 }
194 KeyCode::Char('2') => {
195 app.overlays.view_switcher.hide();
196 mouse::switch_to_view(app, super::app::MultiViewType::Timeline);
197 }
198 KeyCode::Char('3') => {
199 app.overlays.view_switcher.hide();
200 mouse::switch_to_view(app, super::app::MultiViewType::Matrix);
201 }
202 _ => {}
203 }
204 return;
205 }
206
207 if app.overlays.shortcuts.visible {
209 match key.code {
210 KeyCode::Esc | KeyCode::Char('K') | KeyCode::F(1) => app.overlays.shortcuts.hide(),
211 _ => {}
212 }
213 return;
214 }
215
216 if app.overlays.component_deep_dive.visible {
218 match key.code {
219 KeyCode::Esc => app.overlays.component_deep_dive.close(),
220 KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => {
221 app.overlays.component_deep_dive.next_section()
222 }
223 KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
224 app.overlays.component_deep_dive.prev_section()
225 }
226 _ => {}
227 }
228 return;
229 }
230
231 match key.code {
233 KeyCode::Char('q') => app.should_quit = true,
234 KeyCode::Char('?') => app.toggle_help(),
235 KeyCode::Char('e') => app.toggle_export(),
236 KeyCode::Char('l') => app.toggle_legend(),
237 KeyCode::Char('T') => {
238 let theme_name = toggle_theme();
240 let prefs = TuiPreferences {
241 theme: theme_name.to_string(),
242 };
243 let _ = prefs.save();
244 }
245 KeyCode::Char('V') => {
247 if matches!(
248 app.mode,
249 super::AppMode::MultiDiff | super::AppMode::Timeline | super::AppMode::Matrix
250 ) {
251 app.overlays.view_switcher.toggle();
252 }
253 }
254 KeyCode::Char('M') => {
256 if matches!(app.mode, super::AppMode::Diff) {
257 app.toggle_threshold_tuning();
258 }
259 }
260 KeyCode::Char('K') | KeyCode::F(1) => {
262 let context = match app.mode {
263 super::AppMode::MultiDiff => super::app::ShortcutsContext::MultiDiff,
264 super::AppMode::Timeline => super::app::ShortcutsContext::Timeline,
265 super::AppMode::Matrix => super::app::ShortcutsContext::Matrix,
266 super::AppMode::Diff => super::app::ShortcutsContext::Diff,
267 super::AppMode::View => super::app::ShortcutsContext::Global,
268 };
269 app.overlays.shortcuts.show(context);
270 }
271 KeyCode::Char('D') => {
273 if let Some(component_name) = helpers::get_selected_component_name(app) {
274 app.overlays.component_deep_dive.open(component_name, None);
275 }
276 }
277 KeyCode::Char('P') => {
279 if matches!(app.mode, super::AppMode::Diff | super::AppMode::View) {
280 app.run_compliance_check();
281 }
282 }
283 KeyCode::Char('p') => {
285 if matches!(app.mode, super::AppMode::Diff | super::AppMode::View) {
286 app.next_policy();
287 }
288 }
289 KeyCode::Esc => app.close_overlays(),
290 KeyCode::Char('b') | KeyCode::Backspace => {
291 if app.has_navigation_history() {
293 app.navigate_back();
294 }
295 }
296 KeyCode::Tab => {
297 if key.modifiers.contains(KeyModifiers::SHIFT) {
298 app.prev_tab();
299 } else {
300 app.next_tab();
301 }
302 }
303 KeyCode::Char('/') => app.start_search(),
304 KeyCode::Char('1') => app.select_tab(super::TabKind::Summary),
305 KeyCode::Char('2') => app.select_tab(super::TabKind::Components),
306 KeyCode::Char('3') => app.select_tab(super::TabKind::Dependencies),
307 KeyCode::Char('4') => app.select_tab(super::TabKind::Licenses),
308 KeyCode::Char('5') => app.select_tab(super::TabKind::Vulnerabilities),
309 KeyCode::Char('6') => app.select_tab(super::TabKind::Quality),
310 KeyCode::Char('7') => {
311 if app.mode == super::AppMode::Diff {
313 app.select_tab(super::TabKind::Compliance);
314 }
315 }
316 KeyCode::Char('8') => {
317 if app.mode == super::AppMode::Diff {
319 app.select_tab(super::TabKind::SideBySide);
320 }
321 }
322 KeyCode::Char('9') => {
323 if let Some(ref result) = app.data.diff_result {
325 if !result.graph_changes.is_empty() {
326 app.select_tab(super::TabKind::GraphChanges);
327 }
328 }
329 }
330 KeyCode::Char('0') => {
331 if app.mode == super::AppMode::Diff {
333 app.select_tab(super::TabKind::Source);
334 }
335 }
336 KeyCode::Up | KeyCode::Char('k') => app.select_up(),
338 KeyCode::Down | KeyCode::Char('j') => app.select_down(),
339 KeyCode::PageUp => app.page_up(),
340 KeyCode::PageDown => app.page_down(),
341 KeyCode::Home | KeyCode::Char('g') if !key.modifiers.contains(KeyModifiers::SHIFT) => {
342 app.select_first()
343 }
344 KeyCode::End | KeyCode::Char('G') => app.select_last(),
345 _ => {}
346 }
347
348 match app.active_tab {
350 super::TabKind::Components => components::handle_components_keys(app, key),
351 super::TabKind::Dependencies => dependencies::handle_dependencies_keys(app, key),
352 super::TabKind::Licenses => licenses::handle_licenses_keys(app, key),
353 super::TabKind::Vulnerabilities => vulnerabilities::handle_vulnerabilities_keys(app, key),
354 super::TabKind::Quality => quality::handle_quality_keys(app, key),
355 super::TabKind::Compliance => compliance::handle_diff_compliance_keys(app, key),
356 super::TabKind::GraphChanges => graph_changes::handle_graph_changes_keys(app, key),
357 super::TabKind::SideBySide => sidebyside::handle_sidebyside_keys(app, key),
358 super::TabKind::Source => source::handle_source_keys(app, key),
359 _ => {}
360 }
361
362 match app.mode {
364 super::AppMode::MultiDiff => multi_diff::handle_multi_diff_keys(app, key),
365 super::AppMode::Timeline => timeline::handle_timeline_keys(app, key),
366 super::AppMode::Matrix => matrix::handle_matrix_keys(app, key),
367 _ => {}
368 }
369}
370