1pub mod app;
12pub mod errors;
13pub mod input;
14pub mod messages;
15pub mod ui;
16pub mod worker;
17
18pub use app::App;
19pub use messages::{Command, SensorEvent};
20pub use worker::SensorWorker;
21
22use std::io::{self, stdout};
23use std::time::Duration;
24
25use anyhow::Result;
26use crossterm::{
27 ExecutableCommand,
28 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind},
29 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
30};
31use ratatui::prelude::*;
32use tokio::sync::mpsc;
33use tracing::info;
34
35use aranet_store::default_db_path;
36
37pub fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
41 enable_raw_mode()?;
42 stdout().execute(EnterAlternateScreen)?;
43 stdout().execute(EnableMouseCapture)?;
44 let backend = CrosstermBackend::new(stdout());
45 let terminal = Terminal::new(backend)?;
46 Ok(terminal)
47}
48
49pub fn restore_terminal() -> Result<()> {
53 stdout().execute(DisableMouseCapture)?;
54 disable_raw_mode()?;
55 stdout().execute(LeaveAlternateScreen)?;
56 Ok(())
57}
58
59pub async fn run() -> Result<()> {
68 let (cmd_tx, cmd_rx) = mpsc::channel::<Command>(32);
70 let (event_tx, event_rx) = mpsc::channel::<SensorEvent>(32);
71
72 let store_path = default_db_path();
74 info!("Store path: {:?}", store_path);
75
76 let worker = SensorWorker::new(cmd_rx, event_tx, store_path);
78 let worker_handle = tokio::spawn(worker.run());
79
80 let mut app = App::new(cmd_tx.clone(), event_rx);
82
83 let mut terminal = setup_terminal()?;
85
86 let _ = cmd_tx.try_send(Command::LoadCachedData);
88
89 let _ = cmd_tx.try_send(Command::Scan {
91 duration: Duration::from_secs(5),
92 });
93
94 let result = run_event_loop(&mut terminal, &mut app, &cmd_tx).await;
96
97 let _ = cmd_tx.try_send(Command::Shutdown);
99
100 restore_terminal()?;
102
103 let _ = worker_handle.await;
105
106 result
107}
108
109async fn run_event_loop(
111 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
112 app: &mut App,
113 command_tx: &mpsc::Sender<Command>,
114) -> Result<()> {
115 while !app.should_quit() {
116 app.tick_spinner();
118 app.clean_expired_messages();
119
120 terminal.draw(|f| ui::draw(f, app))?;
122
123 if event::poll(Duration::from_millis(100))? {
125 match event::read()? {
126 Event::Key(key) => {
127 if key.kind == KeyEventKind::Press {
128 let action = input::handle_key(
129 key.code,
130 app.editing_alias,
131 app.pending_confirmation.is_some(),
132 );
133 if let Some(cmd) = input::apply_action(app, action, command_tx) {
134 let _ = command_tx.try_send(cmd);
135 }
136 }
137 }
138 Event::Mouse(mouse_event) => {
139 let action = input::handle_mouse(mouse_event);
140 if let Some(cmd) = input::apply_action(app, action, command_tx) {
141 let _ = command_tx.try_send(cmd);
142 }
143 }
144 _ => {}
145 }
146 }
147
148 while let Ok(event) = app.event_rx.try_recv() {
150 let auto_commands = app.handle_sensor_event(event);
152 for cmd in auto_commands {
153 let _ = command_tx.try_send(cmd);
154 }
155 }
156
157 let devices_to_refresh = app.check_auto_refresh();
159 for device_id in devices_to_refresh {
160 let _ = command_tx.try_send(Command::RefreshReading { device_id });
161 }
162 }
163
164 Ok(())
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crossterm::event::KeyCode;
171
172 #[test]
173 fn test_terminal_functions_exist() {
174 let _ = restore_terminal;
177 let _ = setup_terminal;
178 }
179
180 #[test]
181 fn test_input_handling_quit() {
182 let action = input::handle_key(KeyCode::Char('q'), false, false);
183 assert_eq!(action, input::Action::Quit);
184 }
185
186 #[test]
187 fn test_input_handling_scan() {
188 let action = input::handle_key(KeyCode::Char('s'), false, false);
189 assert_eq!(action, input::Action::Scan);
190 }
191
192 #[test]
193 fn test_input_handling_connect_all() {
194 let action = input::handle_key(KeyCode::Char('c'), false, false);
196 assert_eq!(action, input::Action::Connect);
197
198 let action = input::handle_key(KeyCode::Char('C'), false, false);
200 assert_eq!(action, input::Action::ConnectAll);
201 }
202
203 #[test]
204 fn test_input_handling_other_keys() {
205 let action = input::handle_key(KeyCode::Char('a'), false, false);
206 assert_eq!(action, input::Action::ToggleAlertHistory);
208
209 let action = input::handle_key(KeyCode::Enter, false, false);
211 assert_eq!(action, input::Action::ChangeSetting);
212 }
213
214 #[test]
215 fn test_input_handling_confirmation() {
216 let action = input::handle_key(KeyCode::Char('y'), false, true);
218 assert_eq!(action, input::Action::Confirm);
219
220 let action = input::handle_key(KeyCode::Char('n'), false, true);
221 assert_eq!(action, input::Action::Cancel);
222
223 let action = input::handle_key(KeyCode::Esc, false, true);
224 assert_eq!(action, input::Action::Cancel);
225
226 let action = input::handle_key(KeyCode::Char('q'), false, true);
228 assert_eq!(action, input::Action::None);
229 }
230}