//! System tray icon and menu for the agent //! //! Provides a tray icon with menu options: //! - Connection status //! - Machine name //! - End session use anyhow::Result; use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tray_icon::{Icon, TrayIcon, TrayIconBuilder, TrayIconEvent}; use tracing::{info, warn}; #[cfg(windows)] use windows::Win32::UI::WindowsAndMessaging::{ PeekMessageW, TranslateMessage, DispatchMessageW, MSG, PM_REMOVE, }; /// Events that can be triggered from the tray menu #[derive(Debug, Clone)] pub enum TrayAction { EndSession, ShowDetails, ShowDebugWindow, } /// Tray icon controller pub struct TrayController { _tray_icon: TrayIcon, menu: Menu, end_session_item: MenuItem, debug_item: MenuItem, status_item: MenuItem, exit_requested: Arc, } impl TrayController { /// Create a new tray controller /// `allow_end_session` - If true, show "End Session" menu item (only for support sessions) pub fn new(machine_name: &str, support_code: Option<&str>, allow_end_session: bool) -> Result { // Create menu items let status_text = if let Some(code) = support_code { format!("Support Session: {}", code) } else { "Persistent Agent".to_string() }; let status_item = MenuItem::new(&status_text, false, None); let machine_item = MenuItem::new(format!("Machine: {}", machine_name), false, None); let separator = PredefinedMenuItem::separator(); // Only show "End Session" for support sessions // Persistent agents can only be removed by admin let end_session_item = if allow_end_session { MenuItem::new("End Session", true, None) } else { MenuItem::new("Managed by Administrator", false, None) }; // Debug window option (always available) let debug_item = MenuItem::new("Show Debug Window", true, None); // Build menu let menu = Menu::new(); menu.append(&status_item)?; menu.append(&machine_item)?; menu.append(&separator)?; menu.append(&debug_item)?; menu.append(&end_session_item)?; // Create tray icon let icon = create_default_icon()?; let tray_icon = TrayIconBuilder::new() .with_menu(Box::new(menu.clone())) .with_tooltip(format!("GuruConnect - {}", machine_name)) .with_icon(icon) .build()?; let exit_requested = Arc::new(AtomicBool::new(false)); Ok(Self { _tray_icon: tray_icon, menu, end_session_item, debug_item, status_item, exit_requested, }) } /// Check if exit has been requested pub fn exit_requested(&self) -> bool { self.exit_requested.load(Ordering::SeqCst) } /// Update the connection status display pub fn update_status(&self, status: &str) { self.status_item.set_text(status); } /// Process pending menu events (call this from the main loop) pub fn process_events(&self) -> Option { // Pump Windows message queue to process tray icon events #[cfg(windows)] pump_windows_messages(); // Check for menu events if let Ok(event) = MenuEvent::receiver().try_recv() { if event.id == self.end_session_item.id() { info!("End session requested from tray menu"); self.exit_requested.store(true, Ordering::SeqCst); return Some(TrayAction::EndSession); } if event.id == self.debug_item.id() { info!("Debug window requested from tray menu"); return Some(TrayAction::ShowDebugWindow); } } // Check for tray icon events (like double-click) if let Ok(event) = TrayIconEvent::receiver().try_recv() { match event { TrayIconEvent::DoubleClick { .. } => { info!("Tray icon double-clicked"); return Some(TrayAction::ShowDetails); } _ => {} } } None } } /// Pump the Windows message queue to process tray icon events #[cfg(windows)] fn pump_windows_messages() { unsafe { let mut msg = MSG::default(); // Process all pending messages while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { let _ = TranslateMessage(&msg); DispatchMessageW(&msg); } } } /// Create a simple default icon (green circle for connected) fn create_default_icon() -> Result { // Create a simple 32x32 green icon let size = 32u32; let mut rgba = vec![0u8; (size * size * 4) as usize]; let center = size as f32 / 2.0; let radius = size as f32 / 2.0 - 2.0; for y in 0..size { for x in 0..size { let dx = x as f32 - center; let dy = y as f32 - center; let dist = (dx * dx + dy * dy).sqrt(); let idx = ((y * size + x) * 4) as usize; if dist <= radius { // Green circle rgba[idx] = 76; // R rgba[idx + 1] = 175; // G rgba[idx + 2] = 80; // B rgba[idx + 3] = 255; // A } else if dist <= radius + 1.0 { // Anti-aliased edge let alpha = ((radius + 1.0 - dist) * 255.0) as u8; rgba[idx] = 76; rgba[idx + 1] = 175; rgba[idx + 2] = 80; rgba[idx + 3] = alpha; } } } let icon = Icon::from_rgba(rgba, size, size)?; Ok(icon) } #[cfg(test)] mod tests { use super::*; #[test] fn test_create_icon() { let icon = create_default_icon(); assert!(icon.is_ok()); } }