1use std::time::Duration;
26
27use crossterm::event::{KeyCode, MouseButton, MouseEvent, MouseEventKind};
28use tokio::sync::mpsc;
29
30use super::app::{App, ConnectionStatus, HistoryFilter, PendingAction, Tab, Theme};
31use super::messages::Command;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Action {
36 Quit,
38 Scan,
40 Refresh,
42 Connect,
44 ConnectAll,
46 Disconnect,
48 SyncHistory,
50 SelectNext,
52 SelectPrevious,
54 NextTab,
56 PreviousTab,
58 ToggleHelp,
60 ToggleLogging,
62 ToggleBell,
64 DismissAlert,
66 ScrollUp,
68 ScrollDown,
70 SetHistoryFilter(HistoryFilter),
72 IncreaseThreshold,
74 DecreaseThreshold,
76 ChangeSetting,
78 ExportHistory,
80 ToggleAlertHistory,
82 CycleDeviceFilter,
84 ToggleSidebar,
86 ToggleSidebarWidth,
88 MouseClick { x: u16, y: u16 },
90 Confirm,
92 Cancel,
94 ToggleChart,
96 EditAlias,
98 TextInput(char),
100 TextBackspace,
102 TextSubmit,
104 TextCancel,
106 ToggleStickyAlerts,
108 ToggleComparison,
110 NextComparisonDevice,
112 PrevComparisonDevice,
114 ShowErrorDetails,
116 ToggleTheme,
118 ToggleChartTemp,
120 ToggleChartHumidity,
122 ToggleBleRange,
124 ToggleSmartHome,
126 ToggleDoNotDisturb,
128 ToggleExportFormat,
130 None,
132}
133
134pub fn handle_key(key: KeyCode, editing_text: bool, has_pending_confirmation: bool) -> Action {
147 if editing_text {
149 return match key {
150 KeyCode::Enter => Action::TextSubmit,
151 KeyCode::Esc => Action::TextCancel,
152 KeyCode::Backspace => Action::TextBackspace,
153 KeyCode::Char(c) => Action::TextInput(c),
154 _ => Action::None,
155 };
156 }
157
158 if has_pending_confirmation {
160 return match key {
161 KeyCode::Char('y') | KeyCode::Char('Y') => Action::Confirm,
162 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => Action::Cancel,
163 _ => Action::None,
164 };
165 }
166
167 match key {
168 KeyCode::Char('q') => Action::Quit,
169 KeyCode::Char('s') => Action::Scan,
170 KeyCode::Char('r') => Action::Refresh,
171 KeyCode::Char('c') => Action::Connect,
172 KeyCode::Char('C') => Action::ConnectAll,
173 KeyCode::Char('d') => Action::Disconnect,
174 KeyCode::Char('S') | KeyCode::Char('y') => Action::SyncHistory,
175 KeyCode::Down | KeyCode::Char('j') => Action::SelectNext,
176 KeyCode::Up | KeyCode::Char('k') => Action::SelectPrevious,
177 KeyCode::Tab | KeyCode::Char('l') => Action::NextTab,
178 KeyCode::BackTab | KeyCode::Char('h') => Action::PreviousTab,
179 KeyCode::Char('?') => Action::ToggleHelp,
180 KeyCode::Char('L') => Action::ToggleLogging,
181 KeyCode::Char('b') => Action::ToggleBell,
182 KeyCode::Char('n') => Action::EditAlias,
183 KeyCode::Esc => Action::DismissAlert,
184 KeyCode::PageUp => Action::ScrollUp,
185 KeyCode::PageDown => Action::ScrollDown,
186 KeyCode::Char('0') => Action::SetHistoryFilter(HistoryFilter::All),
187 KeyCode::Char('1') => Action::SetHistoryFilter(HistoryFilter::Today),
188 KeyCode::Char('2') => Action::SetHistoryFilter(HistoryFilter::Last24Hours),
189 KeyCode::Char('3') => Action::SetHistoryFilter(HistoryFilter::Last7Days),
190 KeyCode::Char('4') => Action::SetHistoryFilter(HistoryFilter::Last30Days),
191 KeyCode::Char('+') | KeyCode::Char('=') => Action::IncreaseThreshold,
192 KeyCode::Char('-') | KeyCode::Char('_') => Action::DecreaseThreshold,
193 KeyCode::Enter => Action::ChangeSetting,
194 KeyCode::Char('e') => Action::ExportHistory,
195 KeyCode::Char('a') => Action::ToggleAlertHistory,
196 KeyCode::Char('f') => Action::CycleDeviceFilter,
197 KeyCode::Char('[') => Action::ToggleSidebar,
198 KeyCode::Char(']') => Action::ToggleSidebarWidth,
199 KeyCode::Char('g') => Action::ToggleChart,
200 KeyCode::Char('A') => Action::ToggleStickyAlerts,
201 KeyCode::Char('v') => Action::ToggleComparison,
202 KeyCode::Char('<') => Action::PrevComparisonDevice,
203 KeyCode::Char('>') => Action::NextComparisonDevice,
204 KeyCode::Char('E') => Action::ShowErrorDetails,
205 KeyCode::Char('t') => Action::ToggleTheme,
206 KeyCode::Char('T') => Action::ToggleChartTemp,
207 KeyCode::Char('H') => Action::ToggleChartHumidity,
208 KeyCode::Char('B') => Action::ToggleBleRange,
209 KeyCode::Char('I') => Action::ToggleSmartHome,
210 KeyCode::Char('D') => Action::ToggleDoNotDisturb,
211 KeyCode::Char('F') => Action::ToggleExportFormat,
212 _ => Action::None,
213 }
214}
215
216pub fn handle_mouse(event: MouseEvent) -> Action {
227 match event.kind {
228 MouseEventKind::Down(MouseButton::Left) => Action::MouseClick {
229 x: event.column,
230 y: event.row,
231 },
232 _ => Action::None,
233 }
234}
235
236fn apply_navigation_action(app: &mut App, action: Action) -> Option<Command> {
238 match action {
239 Action::SelectNext => {
240 if app.active_tab == Tab::Settings {
241 app.select_next_setting();
242 } else {
243 app.select_next_device();
244 }
245 None
246 }
247 Action::SelectPrevious => {
248 if app.active_tab == Tab::Settings {
249 app.select_previous_setting();
250 } else {
251 app.select_previous_device();
252 }
253 None
254 }
255 Action::NextTab => {
256 app.active_tab = match app.active_tab {
257 Tab::Dashboard => Tab::History,
258 Tab::History => Tab::Settings,
259 Tab::Settings => Tab::Service,
260 Tab::Service => Tab::Dashboard,
261 };
262 None
263 }
264 Action::PreviousTab => {
265 app.active_tab = match app.active_tab {
266 Tab::Dashboard => Tab::Service,
267 Tab::History => Tab::Dashboard,
268 Tab::Settings => Tab::History,
269 Tab::Service => Tab::Settings,
270 };
271 None
272 }
273 Action::ScrollUp => {
274 if app.active_tab == Tab::History {
275 app.scroll_history_up();
276 }
277 None
278 }
279 Action::ScrollDown => {
280 if app.active_tab == Tab::History {
281 app.scroll_history_down();
282 }
283 None
284 }
285 Action::SetHistoryFilter(filter) => {
286 if app.active_tab == Tab::History {
287 app.set_history_filter(filter);
288 }
289 None
290 }
291 Action::MouseClick { x, y } => {
292 if (1..=3).contains(&y) {
294 if x < 15 {
296 app.active_tab = Tab::Dashboard;
297 } else if x < 30 {
298 app.active_tab = Tab::History;
299 } else if x < 45 {
300 app.active_tab = Tab::Settings;
301 } else if x < 60 {
302 app.active_tab = Tab::Service;
303 }
304 }
305 else if x < 25 && y > 4 {
307 let device_row = (y as usize).saturating_sub(5);
308 app.select_filtered_row(device_row);
309 }
310 None
311 }
312 _ => None,
313 }
314}
315
316fn apply_device_action(app: &mut App, action: Action) -> Option<Command> {
318 match action {
319 Action::Scan => Some(Command::Scan {
320 duration: Duration::from_secs(5),
321 }),
322 Action::Refresh => {
323 if app.active_tab == Tab::Service {
324 Some(Command::RefreshServiceStatus)
325 } else {
326 Some(Command::RefreshAll)
327 }
328 }
329 Action::Connect => app.selected_device().map(|device| Command::Connect {
330 device_id: device.id.clone(),
331 }),
332 Action::ConnectAll => {
333 let first_disconnected = app
334 .devices
335 .iter()
336 .find(|d| matches!(d.status, ConnectionStatus::Disconnected))
337 .map(|d| d.id.clone());
338 let count = app
339 .devices
340 .iter()
341 .filter(|d| matches!(d.status, ConnectionStatus::Disconnected))
342 .count();
343
344 if let Some(device_id) = first_disconnected {
345 app.push_status_message(format!("Connecting... ({} remaining)", count));
346 return Some(Command::Connect { device_id });
347 } else {
348 app.push_status_message("All devices already connected".to_string());
349 }
350 None
351 }
352 Action::Disconnect => {
353 if let Some(device) = app.selected_device()
354 && matches!(device.status, ConnectionStatus::Connected)
355 {
356 let action = PendingAction::Disconnect {
357 device_id: device.id.clone(),
358 device_name: device.name.clone().unwrap_or_else(|| device.id.clone()),
359 };
360 app.request_confirmation(action);
361 }
362 None
363 }
364 Action::SyncHistory => app.selected_device().map(|device| Command::SyncHistory {
365 device_id: device.id.clone(),
366 }),
367 Action::Confirm => {
368 if app.pending_confirmation.is_some() {
369 return app.confirm_action();
370 }
371 None
372 }
373 Action::Cancel => {
374 if app.pending_confirmation.is_some() {
375 app.cancel_confirmation();
376 }
377 None
378 }
379 Action::EditAlias => {
380 app.start_alias_edit();
381 None
382 }
383 Action::TextInput(c) => {
384 if app.editing_alias {
385 app.alias_input_char(c);
386 }
387 None
388 }
389 Action::TextBackspace => {
390 if app.editing_alias {
391 app.alias_input_backspace();
392 }
393 None
394 }
395 Action::TextSubmit => {
396 if app.editing_alias {
397 app.save_alias();
398 }
399 None
400 }
401 Action::TextCancel => {
402 if app.editing_alias {
403 app.cancel_alias_edit();
404 }
405 None
406 }
407 Action::ExportHistory => {
408 if let Some(path) = app.export_history() {
409 app.push_status_message(format!("Exported to {}", path));
410 } else {
411 app.push_status_message("No history to export".to_string());
412 }
413 None
414 }
415 Action::CycleDeviceFilter => {
416 app.cycle_device_filter();
417 None
418 }
419 _ => None,
420 }
421}
422
423fn apply_settings_action(app: &mut App, action: Action) -> Option<Command> {
425 match action {
426 Action::IncreaseThreshold => {
427 if app.active_tab == Tab::Settings {
428 match app.selected_setting {
429 1 => app.increase_co2_threshold(),
430 2 => app.increase_radon_threshold(),
431 _ => {}
432 }
433 app.push_status_message(format!(
434 "CO2: {} ppm, Radon: {} Bq/m³",
435 app.co2_alert_threshold, app.radon_alert_threshold
436 ));
437 }
438 None
439 }
440 Action::DecreaseThreshold => {
441 if app.active_tab == Tab::Settings {
442 match app.selected_setting {
443 1 => app.decrease_co2_threshold(),
444 2 => app.decrease_radon_threshold(),
445 _ => {}
446 }
447 app.push_status_message(format!(
448 "CO2: {} ppm, Radon: {} Bq/m³",
449 app.co2_alert_threshold, app.radon_alert_threshold
450 ));
451 }
452 None
453 }
454 Action::ChangeSetting => {
455 if app.active_tab == Tab::Service {
456 if let Some(ref status) = app.service_status {
457 if status.reachable {
458 if status.collector_running {
459 return Some(Command::StopServiceCollector);
460 } else {
461 return Some(Command::StartServiceCollector);
462 }
463 } else {
464 app.push_status_message("Service not reachable".to_string());
465 }
466 } else {
467 app.push_status_message(
468 "Service status unknown - press 'r' to refresh".to_string(),
469 );
470 }
471 None
472 } else if app.active_tab == Tab::Settings && app.selected_setting == 0 {
473 if let Some((device_id, new_interval)) = app.cycle_interval() {
474 return Some(Command::SetInterval {
475 device_id,
476 interval_secs: new_interval,
477 });
478 }
479 None
480 } else {
481 None
482 }
483 }
484 Action::ToggleLogging => {
485 app.toggle_logging();
486 None
487 }
488 Action::ToggleBell => {
489 app.bell_enabled = !app.bell_enabled;
490 app.push_status_message(format!(
491 "Bell notifications {}",
492 if app.bell_enabled {
493 "enabled"
494 } else {
495 "disabled"
496 }
497 ));
498 None
499 }
500 Action::ToggleAlertHistory => {
501 app.toggle_alert_history();
502 None
503 }
504 Action::ToggleStickyAlerts => {
505 app.toggle_sticky_alerts();
506 None
507 }
508 Action::ToggleBleRange => {
509 app.toggle_ble_range();
510 None
511 }
512 Action::ToggleSmartHome => {
513 app.toggle_smart_home();
514 None
515 }
516 Action::ToggleDoNotDisturb => {
517 app.toggle_do_not_disturb();
518 None
519 }
520 Action::ToggleExportFormat => {
521 app.toggle_export_format();
522 None
523 }
524 _ => None,
525 }
526}
527
528fn apply_view_action(app: &mut App, action: Action) -> Option<Command> {
530 match action {
531 Action::ToggleHelp => {
532 app.show_help = !app.show_help;
533 None
534 }
535 Action::DismissAlert => {
536 if app.show_help {
537 app.show_help = false;
538 } else if app.show_error_details {
539 app.show_error_details = false;
540 } else if let Some(device) = app.selected_device() {
541 let device_id = device.id.clone();
542 app.dismiss_alert(&device_id);
543 }
544 None
545 }
546 Action::ToggleSidebar => {
547 app.toggle_sidebar();
548 app.push_status_message(
549 if app.show_sidebar {
550 "Sidebar shown"
551 } else {
552 "Sidebar hidden"
553 }
554 .to_string(),
555 );
556 None
557 }
558 Action::ToggleSidebarWidth => {
559 app.toggle_sidebar_width();
560 app.push_status_message(format!("Sidebar width: {}", app.sidebar_width));
561 None
562 }
563 Action::ToggleChart => {
564 app.toggle_fullscreen_chart();
565 None
566 }
567 Action::ToggleComparison => {
568 app.toggle_comparison();
569 None
570 }
571 Action::NextComparisonDevice => {
572 app.cycle_comparison_device(true);
573 None
574 }
575 Action::PrevComparisonDevice => {
576 app.cycle_comparison_device(false);
577 None
578 }
579 Action::ShowErrorDetails => {
580 app.toggle_error_details();
581 None
582 }
583 Action::ToggleTheme => {
584 app.toggle_theme();
585 let theme_name = match app.theme {
586 Theme::Dark => "dark",
587 Theme::Light => "light",
588 };
589 app.push_status_message(format!("Theme: {}", theme_name));
590 None
591 }
592 Action::ToggleChartTemp => {
593 app.toggle_chart_metric(App::METRIC_TEMP);
594 let status = if app.chart_shows(App::METRIC_TEMP) {
595 "shown"
596 } else {
597 "hidden"
598 };
599 app.push_status_message(format!("Temperature on chart: {}", status));
600 None
601 }
602 Action::ToggleChartHumidity => {
603 app.toggle_chart_metric(App::METRIC_HUMIDITY);
604 let status = if app.chart_shows(App::METRIC_HUMIDITY) {
605 "shown"
606 } else {
607 "hidden"
608 };
609 app.push_status_message(format!("Humidity on chart: {}", status));
610 None
611 }
612 _ => None,
613 }
614}
615
616pub fn apply_action(
634 app: &mut App,
635 action: Action,
636 _command_tx: &mpsc::Sender<Command>,
637) -> Option<Command> {
638 match action {
639 Action::Quit => {
640 app.should_quit = true;
641 None
642 }
643 Action::None => None,
644
645 Action::SelectNext
647 | Action::SelectPrevious
648 | Action::NextTab
649 | Action::PreviousTab
650 | Action::ScrollUp
651 | Action::ScrollDown
652 | Action::SetHistoryFilter(_)
653 | Action::MouseClick { .. } => apply_navigation_action(app, action),
654
655 Action::Scan
657 | Action::Refresh
658 | Action::Connect
659 | Action::ConnectAll
660 | Action::Disconnect
661 | Action::SyncHistory
662 | Action::Confirm
663 | Action::Cancel
664 | Action::EditAlias
665 | Action::TextInput(_)
666 | Action::TextBackspace
667 | Action::TextSubmit
668 | Action::TextCancel
669 | Action::ExportHistory
670 | Action::CycleDeviceFilter => apply_device_action(app, action),
671
672 Action::IncreaseThreshold
674 | Action::DecreaseThreshold
675 | Action::ChangeSetting
676 | Action::ToggleLogging
677 | Action::ToggleBell
678 | Action::ToggleAlertHistory
679 | Action::ToggleStickyAlerts
680 | Action::ToggleBleRange
681 | Action::ToggleSmartHome
682 | Action::ToggleDoNotDisturb
683 | Action::ToggleExportFormat => apply_settings_action(app, action),
684
685 Action::ToggleHelp
687 | Action::DismissAlert
688 | Action::ToggleSidebar
689 | Action::ToggleSidebarWidth
690 | Action::ToggleChart
691 | Action::ToggleComparison
692 | Action::NextComparisonDevice
693 | Action::PrevComparisonDevice
694 | Action::ShowErrorDetails
695 | Action::ToggleTheme
696 | Action::ToggleChartTemp
697 | Action::ToggleChartHumidity => apply_view_action(app, action),
698 }
699}