use std::time::Duration;
use crossterm::event::{KeyCode, MouseButton, MouseEvent, MouseEventKind};
use tokio::sync::mpsc;
use super::app::{App, ConnectionStatus, HistoryFilter, PendingAction, Tab, Theme};
use super::messages::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Quit,
Scan,
Refresh,
Connect,
ConnectAll,
Disconnect,
SyncHistory,
SelectNext,
SelectPrevious,
NextTab,
PreviousTab,
ToggleHelp,
ToggleLogging,
ToggleBell,
DismissAlert,
ScrollUp,
ScrollDown,
SetHistoryFilter(HistoryFilter),
IncreaseThreshold,
DecreaseThreshold,
ChangeSetting,
ExportHistory,
ToggleAlertHistory,
CycleDeviceFilter,
ToggleSidebar,
ToggleSidebarWidth,
MouseClick { x: u16, y: u16 },
Confirm,
Cancel,
ToggleChart,
EditAlias,
TextInput(char),
TextBackspace,
TextSubmit,
TextCancel,
ToggleStickyAlerts,
ToggleComparison,
NextComparisonDevice,
PrevComparisonDevice,
ShowErrorDetails,
ToggleTheme,
ToggleChartTemp,
ToggleChartHumidity,
ToggleBleRange,
ToggleSmartHome,
ToggleDoNotDisturb,
ToggleExportFormat,
None,
}
pub fn handle_key(key: KeyCode, editing_text: bool, has_pending_confirmation: bool) -> Action {
if editing_text {
return match key {
KeyCode::Enter => Action::TextSubmit,
KeyCode::Esc => Action::TextCancel,
KeyCode::Backspace => Action::TextBackspace,
KeyCode::Char(c) => Action::TextInput(c),
_ => Action::None,
};
}
if has_pending_confirmation {
return match key {
KeyCode::Char('y') | KeyCode::Char('Y') => Action::Confirm,
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => Action::Cancel,
_ => Action::None,
};
}
match key {
KeyCode::Char('q') => Action::Quit,
KeyCode::Char('s') => Action::Scan,
KeyCode::Char('r') => Action::Refresh,
KeyCode::Char('c') => Action::Connect,
KeyCode::Char('C') => Action::ConnectAll,
KeyCode::Char('d') => Action::Disconnect,
KeyCode::Char('S') | KeyCode::Char('y') => Action::SyncHistory,
KeyCode::Down | KeyCode::Char('j') => Action::SelectNext,
KeyCode::Up | KeyCode::Char('k') => Action::SelectPrevious,
KeyCode::Tab | KeyCode::Char('l') => Action::NextTab,
KeyCode::BackTab | KeyCode::Char('h') => Action::PreviousTab,
KeyCode::Char('?') => Action::ToggleHelp,
KeyCode::Char('L') => Action::ToggleLogging,
KeyCode::Char('b') => Action::ToggleBell,
KeyCode::Char('n') => Action::EditAlias,
KeyCode::Esc => Action::DismissAlert,
KeyCode::PageUp => Action::ScrollUp,
KeyCode::PageDown => Action::ScrollDown,
KeyCode::Char('0') => Action::SetHistoryFilter(HistoryFilter::All),
KeyCode::Char('1') => Action::SetHistoryFilter(HistoryFilter::Today),
KeyCode::Char('2') => Action::SetHistoryFilter(HistoryFilter::Last24Hours),
KeyCode::Char('3') => Action::SetHistoryFilter(HistoryFilter::Last7Days),
KeyCode::Char('4') => Action::SetHistoryFilter(HistoryFilter::Last30Days),
KeyCode::Char('+') | KeyCode::Char('=') => Action::IncreaseThreshold,
KeyCode::Char('-') | KeyCode::Char('_') => Action::DecreaseThreshold,
KeyCode::Enter => Action::ChangeSetting,
KeyCode::Char('e') => Action::ExportHistory,
KeyCode::Char('a') => Action::ToggleAlertHistory,
KeyCode::Char('f') => Action::CycleDeviceFilter,
KeyCode::Char('[') => Action::ToggleSidebar,
KeyCode::Char(']') => Action::ToggleSidebarWidth,
KeyCode::Char('g') => Action::ToggleChart,
KeyCode::Char('A') => Action::ToggleStickyAlerts,
KeyCode::Char('v') => Action::ToggleComparison,
KeyCode::Char('<') => Action::PrevComparisonDevice,
KeyCode::Char('>') => Action::NextComparisonDevice,
KeyCode::Char('E') => Action::ShowErrorDetails,
KeyCode::Char('t') => Action::ToggleTheme,
KeyCode::Char('T') => Action::ToggleChartTemp,
KeyCode::Char('H') => Action::ToggleChartHumidity,
KeyCode::Char('B') => Action::ToggleBleRange,
KeyCode::Char('I') => Action::ToggleSmartHome,
KeyCode::Char('D') => Action::ToggleDoNotDisturb,
KeyCode::Char('F') => Action::ToggleExportFormat,
_ => Action::None,
}
}
pub fn handle_mouse(event: MouseEvent) -> Action {
match event.kind {
MouseEventKind::Down(MouseButton::Left) => Action::MouseClick {
x: event.column,
y: event.row,
},
_ => Action::None,
}
}
fn apply_navigation_action(app: &mut App, action: Action) -> Option<Command> {
match action {
Action::SelectNext => {
if app.active_tab == Tab::Settings {
app.select_next_setting();
} else {
app.select_next_device();
}
None
}
Action::SelectPrevious => {
if app.active_tab == Tab::Settings {
app.select_previous_setting();
} else {
app.select_previous_device();
}
None
}
Action::NextTab => {
app.active_tab = match app.active_tab {
Tab::Dashboard => Tab::History,
Tab::History => Tab::Settings,
Tab::Settings => Tab::Service,
Tab::Service => Tab::Dashboard,
};
None
}
Action::PreviousTab => {
app.active_tab = match app.active_tab {
Tab::Dashboard => Tab::Service,
Tab::History => Tab::Dashboard,
Tab::Settings => Tab::History,
Tab::Service => Tab::Settings,
};
None
}
Action::ScrollUp => {
if app.active_tab == Tab::History {
app.scroll_history_up();
}
None
}
Action::ScrollDown => {
if app.active_tab == Tab::History {
app.scroll_history_down();
}
None
}
Action::SetHistoryFilter(filter) => {
if app.active_tab == Tab::History {
app.set_history_filter(filter);
}
None
}
Action::MouseClick { x, y } => {
if (1..=3).contains(&y) {
if x < 15 {
app.active_tab = Tab::Dashboard;
} else if x < 30 {
app.active_tab = Tab::History;
} else if x < 45 {
app.active_tab = Tab::Settings;
} else if x < 60 {
app.active_tab = Tab::Service;
}
}
else if x < 25 && y > 4 {
let device_row = (y as usize).saturating_sub(5);
app.select_filtered_row(device_row);
}
None
}
_ => None,
}
}
fn apply_device_action(app: &mut App, action: Action) -> Option<Command> {
match action {
Action::Scan => Some(Command::Scan {
duration: Duration::from_secs(5),
}),
Action::Refresh => {
if app.active_tab == Tab::Service {
Some(Command::RefreshServiceStatus)
} else {
Some(Command::RefreshAll)
}
}
Action::Connect => app.selected_device().map(|device| Command::Connect {
device_id: device.id.clone(),
}),
Action::ConnectAll => {
let first_disconnected = app
.devices
.iter()
.find(|d| matches!(d.status, ConnectionStatus::Disconnected))
.map(|d| d.id.clone());
let count = app
.devices
.iter()
.filter(|d| matches!(d.status, ConnectionStatus::Disconnected))
.count();
if let Some(device_id) = first_disconnected {
app.push_status_message(format!("Connecting... ({} remaining)", count));
return Some(Command::Connect { device_id });
} else {
app.push_status_message("All devices already connected".to_string());
}
None
}
Action::Disconnect => {
if let Some(device) = app.selected_device()
&& matches!(device.status, ConnectionStatus::Connected)
{
let action = PendingAction::Disconnect {
device_id: device.id.clone(),
device_name: device.name.clone().unwrap_or_else(|| device.id.clone()),
};
app.request_confirmation(action);
}
None
}
Action::SyncHistory => app.selected_device().map(|device| Command::SyncHistory {
device_id: device.id.clone(),
}),
Action::Confirm => {
if app.pending_confirmation.is_some() {
return app.confirm_action();
}
None
}
Action::Cancel => {
if app.pending_confirmation.is_some() {
app.cancel_confirmation();
}
None
}
Action::EditAlias => {
app.start_alias_edit();
None
}
Action::TextInput(c) => {
if app.editing_alias {
app.alias_input_char(c);
}
None
}
Action::TextBackspace => {
if app.editing_alias {
app.alias_input_backspace();
}
None
}
Action::TextSubmit => {
if app.editing_alias {
app.save_alias();
}
None
}
Action::TextCancel => {
if app.editing_alias {
app.cancel_alias_edit();
}
None
}
Action::ExportHistory => {
if let Some(path) = app.export_history() {
app.push_status_message(format!("Exported to {}", path));
} else {
app.push_status_message("No history to export".to_string());
}
None
}
Action::CycleDeviceFilter => {
app.cycle_device_filter();
None
}
_ => None,
}
}
fn apply_settings_action(app: &mut App, action: Action) -> Option<Command> {
match action {
Action::IncreaseThreshold => {
if app.active_tab == Tab::Settings {
match app.selected_setting {
1 => app.increase_co2_threshold(),
2 => app.increase_radon_threshold(),
_ => {}
}
app.push_status_message(format!(
"CO2: {} ppm, Radon: {} Bq/m³",
app.co2_alert_threshold, app.radon_alert_threshold
));
}
None
}
Action::DecreaseThreshold => {
if app.active_tab == Tab::Settings {
match app.selected_setting {
1 => app.decrease_co2_threshold(),
2 => app.decrease_radon_threshold(),
_ => {}
}
app.push_status_message(format!(
"CO2: {} ppm, Radon: {} Bq/m³",
app.co2_alert_threshold, app.radon_alert_threshold
));
}
None
}
Action::ChangeSetting => {
if app.active_tab == Tab::Service {
if let Some(ref status) = app.service_status {
if status.reachable {
if status.collector_running {
return Some(Command::StopServiceCollector);
} else {
return Some(Command::StartServiceCollector);
}
} else {
app.push_status_message("Service not reachable".to_string());
}
} else {
app.push_status_message(
"Service status unknown - press 'r' to refresh".to_string(),
);
}
None
} else if app.active_tab == Tab::Settings && app.selected_setting == 0 {
if let Some((device_id, new_interval)) = app.cycle_interval() {
return Some(Command::SetInterval {
device_id,
interval_secs: new_interval,
});
}
None
} else {
None
}
}
Action::ToggleLogging => {
app.toggle_logging();
None
}
Action::ToggleBell => {
app.bell_enabled = !app.bell_enabled;
app.push_status_message(format!(
"Bell notifications {}",
if app.bell_enabled {
"enabled"
} else {
"disabled"
}
));
None
}
Action::ToggleAlertHistory => {
app.toggle_alert_history();
None
}
Action::ToggleStickyAlerts => {
app.toggle_sticky_alerts();
None
}
Action::ToggleBleRange => {
app.toggle_ble_range();
None
}
Action::ToggleSmartHome => {
app.toggle_smart_home();
None
}
Action::ToggleDoNotDisturb => {
app.toggle_do_not_disturb();
None
}
Action::ToggleExportFormat => {
app.toggle_export_format();
None
}
_ => None,
}
}
fn apply_view_action(app: &mut App, action: Action) -> Option<Command> {
match action {
Action::ToggleHelp => {
app.show_help = !app.show_help;
None
}
Action::DismissAlert => {
if app.show_help {
app.show_help = false;
} else if app.show_error_details {
app.show_error_details = false;
} else if let Some(device) = app.selected_device() {
let device_id = device.id.clone();
app.dismiss_alert(&device_id);
}
None
}
Action::ToggleSidebar => {
app.toggle_sidebar();
app.push_status_message(
if app.show_sidebar {
"Sidebar shown"
} else {
"Sidebar hidden"
}
.to_string(),
);
None
}
Action::ToggleSidebarWidth => {
app.toggle_sidebar_width();
app.push_status_message(format!("Sidebar width: {}", app.sidebar_width));
None
}
Action::ToggleChart => {
app.toggle_fullscreen_chart();
None
}
Action::ToggleComparison => {
app.toggle_comparison();
None
}
Action::NextComparisonDevice => {
app.cycle_comparison_device(true);
None
}
Action::PrevComparisonDevice => {
app.cycle_comparison_device(false);
None
}
Action::ShowErrorDetails => {
app.toggle_error_details();
None
}
Action::ToggleTheme => {
app.toggle_theme();
let theme_name = match app.theme {
Theme::Dark => "dark",
Theme::Light => "light",
};
app.push_status_message(format!("Theme: {}", theme_name));
None
}
Action::ToggleChartTemp => {
app.toggle_chart_metric(App::METRIC_TEMP);
let status = if app.chart_shows(App::METRIC_TEMP) {
"shown"
} else {
"hidden"
};
app.push_status_message(format!("Temperature on chart: {}", status));
None
}
Action::ToggleChartHumidity => {
app.toggle_chart_metric(App::METRIC_HUMIDITY);
let status = if app.chart_shows(App::METRIC_HUMIDITY) {
"shown"
} else {
"hidden"
};
app.push_status_message(format!("Humidity on chart: {}", status));
None
}
_ => None,
}
}
pub fn apply_action(
app: &mut App,
action: Action,
_command_tx: &mpsc::Sender<Command>,
) -> Option<Command> {
match action {
Action::Quit => {
app.should_quit = true;
None
}
Action::None => None,
Action::SelectNext
| Action::SelectPrevious
| Action::NextTab
| Action::PreviousTab
| Action::ScrollUp
| Action::ScrollDown
| Action::SetHistoryFilter(_)
| Action::MouseClick { .. } => apply_navigation_action(app, action),
Action::Scan
| Action::Refresh
| Action::Connect
| Action::ConnectAll
| Action::Disconnect
| Action::SyncHistory
| Action::Confirm
| Action::Cancel
| Action::EditAlias
| Action::TextInput(_)
| Action::TextBackspace
| Action::TextSubmit
| Action::TextCancel
| Action::ExportHistory
| Action::CycleDeviceFilter => apply_device_action(app, action),
Action::IncreaseThreshold
| Action::DecreaseThreshold
| Action::ChangeSetting
| Action::ToggleLogging
| Action::ToggleBell
| Action::ToggleAlertHistory
| Action::ToggleStickyAlerts
| Action::ToggleBleRange
| Action::ToggleSmartHome
| Action::ToggleDoNotDisturb
| Action::ToggleExportFormat => apply_settings_action(app, action),
Action::ToggleHelp
| Action::DismissAlert
| Action::ToggleSidebar
| Action::ToggleSidebarWidth
| Action::ToggleChart
| Action::ToggleComparison
| Action::NextComparisonDevice
| Action::PrevComparisonDevice
| Action::ShowErrorDetails
| Action::ToggleTheme
| Action::ToggleChartTemp
| Action::ToggleChartHumidity => apply_view_action(app, action),
}
}