mod app;
mod components;
pub mod demo;
mod export;
mod helpers;
mod menu;
mod panels;
mod readings;
mod theme;
mod tray;
mod types;
mod worker;
use std::path::PathBuf;
use std::sync::mpsc as std_mpsc;
use std::sync::{Arc, Mutex};
use anyhow::Result;
use aranet_store::default_db_path;
use eframe::egui::{self, IconData};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use aranet_core::messages::{Command, SensorEvent};
use crate::config::Config;
pub(crate) const ICON_PNG: &[u8] = include_bytes!("../../assets/aranet-icon.png");
fn load_icon() -> Option<Arc<IconData>> {
let img = image::load_from_memory(ICON_PNG).ok()?.into_rgba8();
let (width, height) = img.dimensions();
Some(Arc::new(IconData {
rgba: img.into_raw(),
width,
height,
}))
}
pub use app::AranetApp;
pub use menu::{MenuCommand, MenuManager};
pub use theme::{Theme, ThemeMode};
pub use tray::{
TrayCommand, TrayError, TrayManager, TrayState, check_co2_threshold, hide_dock_icon,
set_egui_context, show_dock_icon,
};
pub use types::{
AlertEntry, AlertSeverity, AlertType, Co2Level, ConnectionFilter, ConnectionState, DeviceState,
DeviceTypeFilter, HistoryFilter, RadonLevel, Tab, Trend,
};
pub use worker::SensorWorker;
#[derive(Debug, Default, Clone)]
pub struct GuiOptions {
pub demo: bool,
pub screenshot: Option<PathBuf>,
pub screenshot_delay_frames: u32,
}
impl GuiOptions {
pub fn demo() -> Self {
Self {
demo: true,
..Default::default()
}
}
pub fn with_screenshot(mut self, path: impl Into<PathBuf>) -> Self {
self.screenshot = Some(path.into());
self
}
}
pub fn run() -> Result<()> {
tracing_subscriber::fmt::init();
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 store_path = default_db_path();
info!("Using database at: {:?}", store_path);
let (command_tx, command_rx) = mpsc::channel::<Command>(32);
let (event_tx, event_rx_tokio) = mpsc::channel::<SensorEvent>(32);
let (std_tx, std_rx) = std_mpsc::channel::<SensorEvent>();
let startup_command_tx = command_tx.clone();
std::thread::spawn(move || {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
tracing::error!("Failed to create tokio runtime: {}", e);
return;
}
};
rt.block_on(async {
let worker = SensorWorker::with_service_config(
command_rx,
event_tx,
store_path,
&service_url,
service_api_key,
);
if let Err(e) = startup_command_tx.send(Command::LoadCachedData).await {
tracing::error!("Failed to send LoadCachedData command at startup: {}", e);
}
if let Err(e) = startup_command_tx.send(Command::RefreshServiceStatus).await {
tracing::warn!(
"Failed to send RefreshServiceStatus command at startup: {}",
e
);
}
let mut event_rx = event_rx_tokio;
let forward_handle = tokio::spawn(async move {
while let Some(event) = event_rx.recv().await {
if std_tx.send(event).is_err() {
break; }
}
});
worker.run().await;
forward_handle.abort();
});
});
let gui_config = &config.gui;
let default_width = 800.0;
let default_height = 600.0;
let window_width = gui_config.window_width.unwrap_or(default_width);
let window_height = gui_config.window_height.unwrap_or(default_height);
let tray_state_temp = Arc::new(Mutex::new(TrayState {
window_visible: true, ..Default::default()
}));
let tray_manager = match TrayManager::new(tray_state_temp.clone()) {
Ok(manager) => Some(manager),
Err(e) => {
warn!(
"Failed to create system tray: {}. Continuing without tray.",
e
);
None
}
};
let start_minimized = gui_config.start_minimized && tray_manager.is_some();
if start_minimized {
info!("Starting minimized to system tray");
if let Ok(mut state) = tray_state_temp.lock() {
state.window_visible = false;
}
hide_dock_icon();
}
let tray_state = tray_state_temp;
let mut viewport = egui::ViewportBuilder::default()
.with_inner_size([window_width, window_height])
.with_min_inner_size([600.0, 400.0])
.with_close_button(true)
.with_visible(!start_minimized);
if let (Some(x), Some(y)) = (gui_config.window_x, gui_config.window_y) {
if x >= -500.0 && y >= -500.0 && x < 5000.0 && y < 5000.0 {
debug!("Restoring window position: ({}, {})", x, y);
viewport = viewport.with_position([x, y]);
}
}
if let Some(icon) = load_icon() {
viewport = viewport.with_icon(icon);
}
let native_options = eframe::NativeOptions {
viewport,
..Default::default()
};
eframe::run_native(
"Aranet",
native_options,
Box::new(move |cc| {
set_egui_context(cc.egui_ctx.clone());
let mut app = AranetApp::new(cc, command_tx, std_rx, tray_state, tray_manager, None);
let menu_manager = match MenuManager::new() {
Ok(manager) => {
manager.init_for_macos();
Some(manager)
}
Err(e) => {
warn!(
"Failed to create native menu: {}. Continuing without menu.",
e
);
None
}
};
app.set_menu_manager(menu_manager);
Ok(Box::new(app))
}),
)
.map_err(|e| anyhow::anyhow!("Failed to run eframe: {}", e))?;
Ok(())
}
pub fn run_with_options(options: GuiOptions) -> Result<()> {
tracing_subscriber::fmt::init();
if options.demo {
info!("Running in demo mode with mock data");
}
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 store_path = default_db_path();
if !options.demo {
info!("Using database at: {:?}", store_path);
}
let (command_tx, command_rx) = mpsc::channel::<Command>(32);
let (event_tx, event_rx_tokio) = mpsc::channel::<SensorEvent>(32);
let (std_tx, std_rx) = std_mpsc::channel::<SensorEvent>();
let startup_command_tx = command_tx.clone();
let is_demo = options.demo;
std::thread::spawn(move || {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
tracing::error!("Failed to create tokio runtime: {}", e);
return;
}
};
rt.block_on(async {
let worker = SensorWorker::with_service_config(
command_rx,
event_tx,
store_path,
&service_url,
service_api_key,
);
if !is_demo {
if let Err(e) = startup_command_tx.send(Command::LoadCachedData).await {
tracing::error!("Failed to send LoadCachedData command at startup: {}", e);
}
if let Err(e) = startup_command_tx.send(Command::RefreshServiceStatus).await {
tracing::warn!(
"Failed to send RefreshServiceStatus command at startup: {}",
e
);
}
}
let mut event_rx = event_rx_tokio;
let forward_handle = tokio::spawn(async move {
while let Some(event) = event_rx.recv().await {
if std_tx.send(event).is_err() {
break; }
}
});
worker.run().await;
forward_handle.abort();
});
});
let gui_config = &config.gui;
let default_width = 800.0;
let default_height = 600.0;
let screenshot_width = 900.0;
let screenshot_height = 600.0;
let (window_width, window_height) = if options.demo {
(screenshot_width, screenshot_height)
} else {
(
gui_config.window_width.unwrap_or(default_width),
gui_config.window_height.unwrap_or(default_height),
)
};
let tray_state_temp = Arc::new(Mutex::new(TrayState {
window_visible: true, ..Default::default()
}));
let tray_manager = if options.demo {
None
} else {
match TrayManager::new(tray_state_temp.clone()) {
Ok(manager) => Some(manager),
Err(e) => {
warn!(
"Failed to create system tray: {}. Continuing without tray.",
e
);
None
}
}
};
let start_minimized = !options.demo && gui_config.start_minimized && tray_manager.is_some();
if start_minimized {
info!("Starting minimized to system tray");
if let Ok(mut state) = tray_state_temp.lock() {
state.window_visible = false;
}
hide_dock_icon();
}
let tray_state = tray_state_temp;
let mut viewport = egui::ViewportBuilder::default()
.with_inner_size([window_width, window_height])
.with_min_inner_size([600.0, 400.0])
.with_close_button(true)
.with_visible(!start_minimized);
if !options.demo
&& let (Some(x), Some(y)) = (gui_config.window_x, gui_config.window_y)
&& x >= -500.0
&& y >= -500.0
&& x < 5000.0
&& y < 5000.0
{
debug!("Restoring window position: ({}, {})", x, y);
viewport = viewport.with_position([x, y]);
}
if let Some(icon) = load_icon() {
viewport = viewport.with_icon(icon);
}
let native_options = eframe::NativeOptions {
viewport,
..Default::default()
};
let screenshot_path = options.screenshot.clone();
let screenshot_delay = options.screenshot_delay_frames;
let demo_mode = options.demo;
eframe::run_native(
"Aranet",
native_options,
Box::new(move |cc| {
set_egui_context(cc.egui_ctx.clone());
let mut app = AranetApp::new_with_options(
cc,
command_tx,
std_rx,
tray_state,
tray_manager,
None,
demo_mode,
screenshot_path,
screenshot_delay,
);
let menu_manager = if demo_mode {
None
} else {
match MenuManager::new() {
Ok(manager) => {
manager.init_for_macos();
Some(manager)
}
Err(e) => {
warn!(
"Failed to create native menu: {}. Continuing without menu.",
e
);
None
}
}
};
app.set_menu_manager(menu_manager);
Ok(Box::new(app))
}),
)
.map_err(|e| anyhow::anyhow!("Failed to run eframe: {}", e))?;
Ok(())
}