pub mod app;
pub mod errors;
pub mod input;
pub mod messages;
pub mod ui;
pub mod worker;
pub use app::App;
pub use messages::{Command, SensorEvent};
pub use worker::SensorWorker;
use std::io::{self, stdout};
use std::time::Duration;
use anyhow::Result;
use crossterm::{
ExecutableCommand,
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind},
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::prelude::*;
use tokio::sync::mpsc;
use tracing::info;
use aranet_store::default_db_path;
use crate::config::Config;
pub fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
stdout().execute(EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
pub fn restore_terminal() -> Result<()> {
stdout().execute(DisableMouseCapture)?;
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
pub async fn run() -> Result<()> {
let config = Config::load_or_default()?;
let service_url = config.gui.service_url.clone();
let service_api_key = config.gui.service_api_key.clone();
let (cmd_tx, cmd_rx) = mpsc::channel::<Command>(32);
let (event_tx, event_rx) = mpsc::channel::<SensorEvent>(32);
let store_path = default_db_path();
info!("Store path: {:?}", store_path);
let worker = if service_api_key.is_none() && service_url == "http://localhost:8080" {
SensorWorker::new(cmd_rx, event_tx, store_path)
} else {
SensorWorker::with_service_config(
cmd_rx,
event_tx,
store_path,
&service_url,
service_api_key.clone(),
)
};
let worker_handle = tokio::spawn(worker.run());
let mut app = App::new(cmd_tx.clone(), event_rx, service_url, service_api_key);
let mut terminal = setup_terminal()?;
let _ = cmd_tx.try_send(Command::LoadCachedData);
let _ = cmd_tx.try_send(Command::Scan {
duration: Duration::from_secs(5),
});
let result = run_event_loop(&mut terminal, &mut app, &cmd_tx).await;
let _ = cmd_tx.try_send(Command::Shutdown);
restore_terminal()?;
let _ = worker_handle.await;
result
}
async fn run_event_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
command_tx: &mpsc::Sender<Command>,
) -> Result<()> {
while !app.should_quit() {
app.tick_spinner();
app.clean_expired_messages();
terminal.draw(|f| ui::draw(f, app))?;
if event::poll(Duration::from_millis(100))? {
match event::read()? {
Event::Key(key) => {
if key.kind == KeyEventKind::Press {
let action = input::handle_key(
key.code,
app.editing_alias,
app.pending_confirmation.is_some(),
);
if let Some(cmd) = input::apply_action(app, action, command_tx) {
let _ = command_tx.try_send(cmd);
}
}
}
Event::Mouse(mouse_event) => {
let action = input::handle_mouse(mouse_event);
if let Some(cmd) = input::apply_action(app, action, command_tx) {
let _ = command_tx.try_send(cmd);
}
}
_ => {}
}
}
while let Ok(event) = app.event_rx.try_recv() {
let auto_commands = app.handle_sensor_event(event);
for cmd in auto_commands {
let _ = command_tx.try_send(cmd);
}
}
let devices_to_refresh = app.check_auto_refresh();
for device_id in devices_to_refresh {
let _ = command_tx.try_send(Command::RefreshReading { device_id });
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyCode;
#[test]
fn test_terminal_functions_exist() {
let _ = restore_terminal;
let _ = setup_terminal;
}
#[test]
fn test_input_handling_quit() {
let action = input::handle_key(KeyCode::Char('q'), false, false);
assert_eq!(action, input::Action::Quit);
}
#[test]
fn test_input_handling_scan() {
let action = input::handle_key(KeyCode::Char('s'), false, false);
assert_eq!(action, input::Action::Scan);
}
#[test]
fn test_input_handling_connect_all() {
let action = input::handle_key(KeyCode::Char('c'), false, false);
assert_eq!(action, input::Action::Connect);
let action = input::handle_key(KeyCode::Char('C'), false, false);
assert_eq!(action, input::Action::ConnectAll);
}
#[test]
fn test_input_handling_other_keys() {
let action = input::handle_key(KeyCode::Char('a'), false, false);
assert_eq!(action, input::Action::ToggleAlertHistory);
let action = input::handle_key(KeyCode::Enter, false, false);
assert_eq!(action, input::Action::ChangeSetting);
}
#[test]
fn test_input_handling_confirmation() {
let action = input::handle_key(KeyCode::Char('y'), false, true);
assert_eq!(action, input::Action::Confirm);
let action = input::handle_key(KeyCode::Char('n'), false, true);
assert_eq!(action, input::Action::Cancel);
let action = input::handle_key(KeyCode::Esc, false, true);
assert_eq!(action, input::Action::Cancel);
let action = input::handle_key(KeyCode::Char('q'), false, true);
assert_eq!(action, input::Action::None);
}
}