1use std::time::Duration;
24
25use crossterm::event::{KeyCode, MouseButton, MouseEvent, MouseEventKind};
26use tokio::sync::mpsc;
27
28use super::app::{App, ConnectionStatus, HistoryFilter, PendingAction, Tab, Theme};
29use super::messages::Command;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Action {
34 Quit,
36 Scan,
38 Refresh,
40 Connect,
42 ConnectAll,
44 Disconnect,
46 SyncHistory,
48 SelectNext,
50 SelectPrevious,
52 NextTab,
54 PreviousTab,
56 ToggleHelp,
58 ToggleLogging,
60 ToggleBell,
62 DismissAlert,
64 ScrollUp,
66 ScrollDown,
68 SetHistoryFilter(HistoryFilter),
70 IncreaseThreshold,
72 DecreaseThreshold,
74 ChangeSetting,
76 ExportHistory,
78 ToggleAlertHistory,
80 CycleDeviceFilter,
82 ToggleSidebar,
84 ToggleSidebarWidth,
86 MouseClick { x: u16, y: u16 },
88 Confirm,
90 Cancel,
92 ToggleChart,
94 EditAlias,
96 TextInput(char),
98 TextBackspace,
100 TextSubmit,
102 TextCancel,
104 ToggleStickyAlerts,
106 ToggleComparison,
108 NextComparisonDevice,
110 PrevComparisonDevice,
112 ShowErrorDetails,
114 ToggleTheme,
116 ToggleChartTemp,
118 ToggleChartHumidity,
120 ToggleBleRange,
122 ToggleSmartHome,
124 None,
126}
127
128pub fn handle_key(key: KeyCode, editing_text: bool, has_pending_confirmation: bool) -> Action {
141 if editing_text {
143 return match key {
144 KeyCode::Enter => Action::TextSubmit,
145 KeyCode::Esc => Action::TextCancel,
146 KeyCode::Backspace => Action::TextBackspace,
147 KeyCode::Char(c) => Action::TextInput(c),
148 _ => Action::None,
149 };
150 }
151
152 if has_pending_confirmation {
154 return match key {
155 KeyCode::Char('y') | KeyCode::Char('Y') => Action::Confirm,
156 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => Action::Cancel,
157 _ => Action::None,
158 };
159 }
160
161 match key {
162 KeyCode::Char('q') => Action::Quit,
163 KeyCode::Char('s') => Action::Scan,
164 KeyCode::Char('r') => Action::Refresh,
165 KeyCode::Char('c') => Action::Connect,
166 KeyCode::Char('C') => Action::ConnectAll,
167 KeyCode::Char('d') => Action::Disconnect,
168 KeyCode::Char('S') | KeyCode::Char('y') => Action::SyncHistory,
169 KeyCode::Down | KeyCode::Char('j') => Action::SelectNext,
170 KeyCode::Up | KeyCode::Char('k') => Action::SelectPrevious,
171 KeyCode::Tab | KeyCode::Char('l') => Action::NextTab,
172 KeyCode::BackTab | KeyCode::Char('h') => Action::PreviousTab,
173 KeyCode::Char('?') => Action::ToggleHelp,
174 KeyCode::Char('L') => Action::ToggleLogging,
175 KeyCode::Char('b') => Action::ToggleBell,
176 KeyCode::Char('n') => Action::EditAlias,
177 KeyCode::Esc => Action::DismissAlert,
178 KeyCode::PageUp => Action::ScrollUp,
179 KeyCode::PageDown => Action::ScrollDown,
180 KeyCode::Char('0') => Action::SetHistoryFilter(HistoryFilter::All),
181 KeyCode::Char('1') => Action::SetHistoryFilter(HistoryFilter::Today),
182 KeyCode::Char('2') => Action::SetHistoryFilter(HistoryFilter::Last24Hours),
183 KeyCode::Char('3') => Action::SetHistoryFilter(HistoryFilter::Last7Days),
184 KeyCode::Char('4') => Action::SetHistoryFilter(HistoryFilter::Last30Days),
185 KeyCode::Char('+') | KeyCode::Char('=') => Action::IncreaseThreshold,
186 KeyCode::Char('-') | KeyCode::Char('_') => Action::DecreaseThreshold,
187 KeyCode::Enter => Action::ChangeSetting,
188 KeyCode::Char('e') => Action::ExportHistory,
189 KeyCode::Char('a') => Action::ToggleAlertHistory,
190 KeyCode::Char('f') => Action::CycleDeviceFilter,
191 KeyCode::Char('[') => Action::ToggleSidebar,
192 KeyCode::Char(']') => Action::ToggleSidebarWidth,
193 KeyCode::Char('g') => Action::ToggleChart,
194 KeyCode::Char('A') => Action::ToggleStickyAlerts,
195 KeyCode::Char('v') => Action::ToggleComparison,
196 KeyCode::Char('<') => Action::PrevComparisonDevice,
197 KeyCode::Char('>') => Action::NextComparisonDevice,
198 KeyCode::Char('E') => Action::ShowErrorDetails,
199 KeyCode::Char('t') => Action::ToggleTheme,
200 KeyCode::Char('T') => Action::ToggleChartTemp,
201 KeyCode::Char('H') => Action::ToggleChartHumidity,
202 KeyCode::Char('B') => Action::ToggleBleRange,
203 KeyCode::Char('I') => Action::ToggleSmartHome,
204 _ => Action::None,
205 }
206}
207
208pub fn handle_mouse(event: MouseEvent) -> Action {
219 match event.kind {
220 MouseEventKind::Down(MouseButton::Left) => Action::MouseClick {
221 x: event.column,
222 y: event.row,
223 },
224 _ => Action::None,
225 }
226}
227
228pub fn apply_action(
244 app: &mut App,
245 action: Action,
246 _command_tx: &mpsc::Sender<Command>,
247) -> Option<Command> {
248 match action {
249 Action::Quit => {
250 app.should_quit = true;
251 None
252 }
253 Action::Scan => Some(Command::Scan {
254 duration: Duration::from_secs(5),
255 }),
256 Action::Refresh => Some(Command::RefreshAll),
257 Action::Connect => app.selected_device().map(|device| Command::Connect {
258 device_id: device.id.clone(),
259 }),
260 Action::ConnectAll => {
261 let first_disconnected = app
264 .devices
265 .iter()
266 .find(|d| matches!(d.status, ConnectionStatus::Disconnected))
267 .map(|d| d.id.clone());
268 let count = app
269 .devices
270 .iter()
271 .filter(|d| matches!(d.status, ConnectionStatus::Disconnected))
272 .count();
273
274 if let Some(device_id) = first_disconnected {
275 app.push_status_message(format!("Connecting... ({} remaining)", count));
276 return Some(Command::Connect { device_id });
277 } else {
278 app.push_status_message("All devices already connected".to_string());
279 }
280 None
281 }
282 Action::Disconnect => {
283 if let Some(device) = app.selected_device()
284 && matches!(device.status, ConnectionStatus::Connected)
285 {
286 let action = PendingAction::Disconnect {
287 device_id: device.id.clone(),
288 device_name: device.name.clone().unwrap_or_else(|| device.id.clone()),
289 };
290 app.request_confirmation(action);
291 }
292 None
293 }
294 Action::SyncHistory => app.selected_device().map(|device| Command::SyncHistory {
295 device_id: device.id.clone(),
296 }),
297 Action::SelectNext => {
298 if app.active_tab == Tab::Settings {
299 app.select_next_setting();
300 } else {
301 app.select_next_device();
302 }
303 None
304 }
305 Action::SelectPrevious => {
306 if app.active_tab == Tab::Settings {
307 app.select_previous_setting();
308 } else {
309 app.select_previous_device();
310 }
311 None
312 }
313 Action::NextTab => {
314 app.active_tab = match app.active_tab {
315 Tab::Dashboard => Tab::History,
316 Tab::History => Tab::Settings,
317 Tab::Settings => Tab::Dashboard,
318 };
319 None
320 }
321 Action::PreviousTab => {
322 app.active_tab = match app.active_tab {
323 Tab::Dashboard => Tab::Settings,
324 Tab::History => Tab::Dashboard,
325 Tab::Settings => Tab::History,
326 };
327 None
328 }
329 Action::ToggleHelp => {
330 app.show_help = !app.show_help;
331 None
332 }
333 Action::ToggleLogging => {
334 app.toggle_logging();
335 None
336 }
337 Action::ToggleBell => {
338 app.bell_enabled = !app.bell_enabled;
339 app.push_status_message(format!(
340 "Bell notifications {}",
341 if app.bell_enabled {
342 "enabled"
343 } else {
344 "disabled"
345 }
346 ));
347 None
348 }
349 Action::DismissAlert => {
350 if app.show_help {
352 app.show_help = false;
353 } else if app.show_error_details {
355 app.show_error_details = false;
356 } else if let Some(device) = app.selected_device() {
357 let device_id = device.id.clone();
358 app.dismiss_alert(&device_id);
359 }
360 None
361 }
362 Action::ScrollUp => {
363 if app.active_tab == Tab::History {
364 app.scroll_history_up();
365 }
366 None
367 }
368 Action::ScrollDown => {
369 if app.active_tab == Tab::History {
370 app.scroll_history_down();
371 }
372 None
373 }
374 Action::SetHistoryFilter(filter) => {
375 if app.active_tab == Tab::History {
376 app.set_history_filter(filter);
377 }
378 None
379 }
380 Action::IncreaseThreshold => {
381 if app.active_tab == Tab::Settings {
382 match app.selected_setting {
383 1 => app.increase_co2_threshold(),
384 2 => app.increase_radon_threshold(),
385 _ => {}
386 }
387 app.push_status_message(format!(
388 "CO2: {} ppm, Radon: {} Bq/m³",
389 app.co2_alert_threshold, app.radon_alert_threshold
390 ));
391 }
392 None
393 }
394 Action::DecreaseThreshold => {
395 if app.active_tab == Tab::Settings {
396 match app.selected_setting {
397 1 => app.decrease_co2_threshold(),
398 2 => app.decrease_radon_threshold(),
399 _ => {}
400 }
401 app.push_status_message(format!(
402 "CO2: {} ppm, Radon: {} Bq/m³",
403 app.co2_alert_threshold, app.radon_alert_threshold
404 ));
405 }
406 None
407 }
408 Action::ChangeSetting => {
409 if app.active_tab == Tab::Settings && app.selected_setting == 0 {
410 if let Some((device_id, new_interval)) = app.cycle_interval() {
412 return Some(Command::SetInterval {
413 device_id,
414 interval_secs: new_interval,
415 });
416 }
417 }
418 None
419 }
420 Action::ExportHistory => {
421 if let Some(path) = app.export_history() {
422 app.push_status_message(format!("Exported to {}", path));
423 } else {
424 app.push_status_message("No history to export".to_string());
425 }
426 None
427 }
428 Action::ToggleAlertHistory => {
429 app.toggle_alert_history();
430 None
431 }
432 Action::ToggleStickyAlerts => {
433 app.toggle_sticky_alerts();
434 None
435 }
436 Action::CycleDeviceFilter => {
437 app.cycle_device_filter();
438 None
439 }
440 Action::ToggleSidebar => {
441 app.toggle_sidebar();
442 app.push_status_message(
443 if app.show_sidebar {
444 "Sidebar shown"
445 } else {
446 "Sidebar hidden"
447 }
448 .to_string(),
449 );
450 None
451 }
452 Action::ToggleSidebarWidth => {
453 app.toggle_sidebar_width();
454 app.push_status_message(format!("Sidebar width: {}", app.sidebar_width));
455 None
456 }
457 Action::MouseClick { x, y } => {
458 if (1..=3).contains(&y) {
460 if x < 15 {
462 app.active_tab = Tab::Dashboard;
463 } else if x < 30 {
464 app.active_tab = Tab::History;
465 } else if x < 45 {
466 app.active_tab = Tab::Settings;
467 }
468 }
469 else if x < 25 && y > 4 {
471 let device_row = (y as usize).saturating_sub(5);
472 if device_row < app.devices.len() {
473 app.selected_device = device_row;
474 }
475 }
476 None
477 }
478 Action::Confirm => {
479 if app.pending_confirmation.is_some() {
480 return app.confirm_action();
481 }
482 None
483 }
484 Action::Cancel => {
485 if app.pending_confirmation.is_some() {
486 app.cancel_confirmation();
487 }
488 None
489 }
490 Action::ToggleChart => {
491 app.toggle_fullscreen_chart();
492 None
493 }
494 Action::EditAlias => {
495 app.start_alias_edit();
496 None
497 }
498 Action::TextInput(c) => {
499 if app.editing_alias {
500 app.alias_input_char(c);
501 }
502 None
503 }
504 Action::TextBackspace => {
505 if app.editing_alias {
506 app.alias_input_backspace();
507 }
508 None
509 }
510 Action::TextSubmit => {
511 if app.editing_alias {
512 app.save_alias();
513 }
514 None
515 }
516 Action::TextCancel => {
517 if app.editing_alias {
518 app.cancel_alias_edit();
519 }
520 None
521 }
522 Action::ToggleComparison => {
523 app.toggle_comparison();
524 None
525 }
526 Action::NextComparisonDevice => {
527 app.cycle_comparison_device(true);
528 None
529 }
530 Action::PrevComparisonDevice => {
531 app.cycle_comparison_device(false);
532 None
533 }
534 Action::ShowErrorDetails => {
535 app.toggle_error_details();
536 None
537 }
538 Action::ToggleTheme => {
539 app.toggle_theme();
540 let theme_name = match app.theme {
541 Theme::Dark => "dark",
542 Theme::Light => "light",
543 };
544 app.push_status_message(format!("Theme: {}", theme_name));
545 None
546 }
547 Action::ToggleChartTemp => {
548 app.toggle_chart_metric(App::METRIC_TEMP);
549 let status = if app.chart_shows(App::METRIC_TEMP) {
550 "shown"
551 } else {
552 "hidden"
553 };
554 app.push_status_message(format!("Temperature on chart: {}", status));
555 None
556 }
557 Action::ToggleChartHumidity => {
558 app.toggle_chart_metric(App::METRIC_HUMIDITY);
559 let status = if app.chart_shows(App::METRIC_HUMIDITY) {
560 "shown"
561 } else {
562 "hidden"
563 };
564 app.push_status_message(format!("Humidity on chart: {}", status));
565 None
566 }
567 Action::ToggleBleRange => {
568 app.toggle_ble_range();
569 None
570 }
571 Action::ToggleSmartHome => {
572 app.toggle_smart_home();
573 None
574 }
575 Action::None => None,
576 }
577}