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
37use crate::config::Config;
38
39pub fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
43 enable_raw_mode()?;
44 stdout().execute(EnterAlternateScreen)?;
45 stdout().execute(EnableMouseCapture)?;
46 let backend = CrosstermBackend::new(stdout());
47 let terminal = Terminal::new(backend)?;
48 Ok(terminal)
49}
50
51pub fn restore_terminal() -> Result<()> {
55 stdout().execute(DisableMouseCapture)?;
56 disable_raw_mode()?;
57 stdout().execute(LeaveAlternateScreen)?;
58 Ok(())
59}
60
61pub async fn run() -> Result<()> {
70 let config = Config::load_or_default()?;
71 let service_url = config.gui.service_url.clone();
72 let service_api_key = config.gui.service_api_key.clone();
73
74 let (cmd_tx, cmd_rx) = mpsc::channel::<Command>(32);
76 let (event_tx, event_rx) = mpsc::channel::<SensorEvent>(32);
77
78 let store_path = default_db_path();
80 info!("Store path: {:?}", store_path);
81
82 let worker = if service_api_key.is_none() && service_url == "http://localhost:8080" {
84 SensorWorker::new(cmd_rx, event_tx, store_path)
85 } else {
86 SensorWorker::with_service_config(
87 cmd_rx,
88 event_tx,
89 store_path,
90 &service_url,
91 service_api_key.clone(),
92 )
93 };
94 let worker_handle = tokio::spawn(worker.run());
95
96 let mut app = App::new(cmd_tx.clone(), event_rx, service_url, service_api_key);
98
99 let mut terminal = setup_terminal()?;
101
102 let _ = cmd_tx.try_send(Command::LoadCachedData);
104
105 let _ = cmd_tx.try_send(Command::Scan {
107 duration: Duration::from_secs(5),
108 });
109
110 let result = run_event_loop(&mut terminal, &mut app, &cmd_tx).await;
112
113 let _ = cmd_tx.try_send(Command::Shutdown);
115
116 restore_terminal()?;
118
119 let _ = worker_handle.await;
121
122 result
123}
124
125async fn run_event_loop(
127 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
128 app: &mut App,
129 command_tx: &mpsc::Sender<Command>,
130) -> Result<()> {
131 while !app.should_quit() {
132 app.tick_spinner();
134 app.clean_expired_messages();
135
136 terminal.draw(|f| ui::draw(f, app))?;
138
139 if event::poll(Duration::from_millis(100))? {
141 match event::read()? {
142 Event::Key(key) => {
143 if key.kind == KeyEventKind::Press {
144 let action = input::handle_key(
145 key.code,
146 app.editing_alias,
147 app.pending_confirmation.is_some(),
148 );
149 if let Some(cmd) = input::apply_action(app, action, command_tx) {
150 let _ = command_tx.try_send(cmd);
151 }
152 }
153 }
154 Event::Mouse(mouse_event) => {
155 let action = input::handle_mouse(mouse_event);
156 if let Some(cmd) = input::apply_action(app, action, command_tx) {
157 let _ = command_tx.try_send(cmd);
158 }
159 }
160 _ => {}
161 }
162 }
163
164 while let Ok(event) = app.event_rx.try_recv() {
166 let auto_commands = app.handle_sensor_event(event);
168 for cmd in auto_commands {
169 let _ = command_tx.try_send(cmd);
170 }
171 }
172
173 let devices_to_refresh = app.check_auto_refresh();
175 for device_id in devices_to_refresh {
176 let _ = command_tx.try_send(Command::RefreshReading { device_id });
177 }
178 }
179
180 Ok(())
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crossterm::event::KeyCode;
187
188 #[test]
189 fn test_terminal_functions_exist() {
190 let _ = restore_terminal;
193 let _ = setup_terminal;
194 }
195
196 #[test]
197 fn test_input_handling_quit() {
198 let action = input::handle_key(KeyCode::Char('q'), false, false);
199 assert_eq!(action, input::Action::Quit);
200 }
201
202 #[test]
203 fn test_input_handling_scan() {
204 let action = input::handle_key(KeyCode::Char('s'), false, false);
205 assert_eq!(action, input::Action::Scan);
206 }
207
208 #[test]
209 fn test_input_handling_connect_all() {
210 let action = input::handle_key(KeyCode::Char('c'), false, false);
212 assert_eq!(action, input::Action::Connect);
213
214 let action = input::handle_key(KeyCode::Char('C'), false, false);
216 assert_eq!(action, input::Action::ConnectAll);
217 }
218
219 #[test]
220 fn test_input_handling_other_keys() {
221 let action = input::handle_key(KeyCode::Char('a'), false, false);
222 assert_eq!(action, input::Action::ToggleAlertHistory);
224
225 let action = input::handle_key(KeyCode::Enter, false, false);
227 assert_eq!(action, input::Action::ChangeSetting);
228 }
229
230 #[test]
231 fn test_input_handling_confirmation() {
232 let action = input::handle_key(KeyCode::Char('y'), false, true);
234 assert_eq!(action, input::Action::Confirm);
235
236 let action = input::handle_key(KeyCode::Char('n'), false, true);
237 assert_eq!(action, input::Action::Cancel);
238
239 let action = input::handle_key(KeyCode::Esc, false, true);
240 assert_eq!(action, input::Action::Cancel);
241
242 let action = input::handle_key(KeyCode::Char('q'), false, true);
244 assert_eq!(action, input::Action::None);
245 }
246}