//! System tray setup for the OpenFang desktop app. use openfang_kernel::config::openfang_home; use tauri::{ menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, Manager, }; use tauri_plugin_autostart::ManagerExt; use tauri_plugin_notification::NotificationExt; use tracing::{info, warn}; /// Format seconds into a human-readable uptime string. fn format_uptime(secs: u64) -> String { if secs < 60 { format!("{secs}s") } else if secs < 3600 { let m = secs / 60; let s = secs % 60; format!("{m}m {s}s") } else { let h = secs / 3600; let m = (secs % 3600) / 60; format!("{h}h {m}m") } } /// Build and register the system tray icon with enhanced menu. pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { // Action items let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; let browser = MenuItem::with_id(app, "browser", "Open in Browser", true, None::<&str>)?; let sep1 = PredefinedMenuItem::separator(app)?; // Informational items (disabled — display only) let agent_count = if let Some(ks) = app.try_state::() { ks.kernel.registry.list().len() } else { 0 }; let uptime = if let Some(ks) = app.try_state::() { format_uptime(ks.started_at.elapsed().as_secs()) } else { "0s".to_string() }; let agents_info = MenuItem::with_id( app, "agents_info", format!("Agents: {agent_count} running"), false, None::<&str>, )?; let status_info = MenuItem::with_id( app, "status_info", format!("Status: Running ({uptime})"), false, None::<&str>, )?; let sep2 = PredefinedMenuItem::separator(app)?; // Settings items let autostart_enabled = app.autolaunch().is_enabled().unwrap_or(false); let launch_at_login = CheckMenuItem::with_id( app, "launch_at_login", "Launch at Login", true, autostart_enabled, None::<&str>, )?; let check_updates = MenuItem::with_id( app, "check_updates", "Check for Updates...", true, None::<&str>, )?; let open_config = MenuItem::with_id( app, "open_config", "Open Config Directory", true, None::<&str>, )?; let sep3 = PredefinedMenuItem::separator(app)?; let quit = MenuItem::with_id(app, "quit", "Quit OpenFang", true, None::<&str>)?; let menu = Menu::with_items( app, &[ &show, &browser, &sep1, &agents_info, &status_info, &sep2, &launch_at_login, &check_updates, &open_config, &sep3, &quit, ], )?; // Load the tray icon from embedded PNG bytes let tray_icon = tauri::image::Image::from_bytes(include_bytes!("../icons/32x32.png")) .expect("Failed to decode tray icon PNG"); let _tray = TrayIconBuilder::new() .icon(tray_icon) .menu(&menu) .tooltip("OpenFang Agent OS") .on_menu_event(move |app, event| match event.id().as_ref() { "show" => { if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.unminimize(); let _ = w.set_focus(); } } "browser" => { if let Some(port) = app.try_state::() { let url = format!("http://127.0.0.1:{}", port.0); let _ = open::that(&url); } } "launch_at_login" => { let manager = app.autolaunch(); let currently_enabled = manager.is_enabled().unwrap_or(false); if currently_enabled { if let Err(e) = manager.disable() { warn!("Failed to disable autostart: {e}"); } } else if let Err(e) = manager.enable() { warn!("Failed to enable autostart: {e}"); } info!( "Autostart toggled: {}", manager.is_enabled().unwrap_or(false) ); } "check_updates" => { let app_handle = app.clone(); tauri::async_runtime::spawn(async move { // First check what's available match crate::updater::check_for_update(&app_handle).await { Ok(info) if info.available => { let version = info.version.as_deref().unwrap_or("unknown"); // Notify user we're starting install let _ = app_handle .notification() .builder() .title("Installing Update...") .body(format!( "Downloading OpenFang v{version}. App will restart shortly." )) .show(); // Perform install if let Err(e) = crate::updater::download_and_install_update(&app_handle).await { warn!("Manual update install failed: {e}"); let _ = app_handle .notification() .builder() .title("Update Failed") .body(format!("Could not install update: {e}")) .show(); } // If we reach here, install failed (success causes restart) } Ok(_) => { let _ = app_handle .notification() .builder() .title("Up to Date") .body("You're running the latest version of OpenFang.") .show(); } Err(e) => { warn!("Tray update check failed: {e}"); let _ = app_handle .notification() .builder() .title("Update Check Failed") .body("Could not check for updates. Try again later.") .show(); } } }); } "open_config" => { let dir = openfang_home(); let _ = std::fs::create_dir_all(&dir); if let Err(e) = open::that(&dir) { warn!("Failed to open config dir: {e}"); } } "quit" => { info!("Quit requested from system tray"); app.exit(0); } _ => {} }) .on_tray_icon_event(|tray, event| { if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { let app = tray.app_handle(); if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.unminimize(); let _ = w.set_focus(); } } }) .build(app)?; Ok(()) }