use muda::{Menu, MenuItem, PredefinedMenuItem}; use std::process::Command; use std::sync::atomic::{AtomicBool, Ordering}; use tray_icon::{Icon, TrayIcon, TrayIconBuilder}; static GUI_RUNNING: AtomicBool = AtomicBool::new(false); pub fn load_icon() -> Icon { // On Windows, use the full-color icon so it renders well on dark taskbars. // On macOS/Linux, use the template icon (black with alpha) for system light/dark handling. #[cfg(target_os = "windows")] let icon_bytes = include_bytes!("../../icons/tray-icon-win-44.png"); #[cfg(not(target_os = "windows"))] let icon_bytes = include_bytes!("../../icons/tray-icon-44.png"); let image = image::load_from_memory(icon_bytes) .expect("Failed to load icon") .into_rgba8(); let (width, height) = image.dimensions(); let rgba = image.into_raw(); Icon::from_rgba(rgba, width, height).expect("Failed to create icon") } pub struct TrayMenu { pub menu: Menu, pub open_item: MenuItem, pub quit_item: MenuItem, } impl Default for TrayMenu { fn default() -> Self { Self::new() } } impl TrayMenu { pub fn new() -> Self { let menu = Menu::new(); let open_item = MenuItem::new("Open Donut Browser", true, None); let separator = PredefinedMenuItem::separator(); let quit_item = MenuItem::new("Quit Donut Browser", true, None); menu.append(&open_item).unwrap(); menu.append(&separator).unwrap(); menu.append(&quit_item).unwrap(); Self { menu, open_item, quit_item, } } } pub fn create_tray_icon(icon: Icon, menu: &Menu) -> TrayIcon { let builder = TrayIconBuilder::new() .with_icon(icon) .with_tooltip("Donut Browser") .with_menu(Box::new(menu.clone())); // On macOS, template icons are automatically colored by the system for light/dark mode #[cfg(target_os = "macos")] let builder = builder.with_icon_as_template(true); builder.build().expect("Failed to create tray icon") } pub fn open_gui() { if GUI_RUNNING.load(Ordering::SeqCst) { log::info!("GUI already running, activating..."); activate_gui(); return; } log::info!("Opening GUI..."); #[cfg(target_os = "macos")] { let _ = Command::new("open").arg("-a").arg("Donut Browser").spawn(); } #[cfg(target_os = "windows")] { use std::path::PathBuf; // In dev mode, find the main exe next to the daemon binary if let Ok(current_exe) = std::env::current_exe() { if let Some(exe_dir) = current_exe.parent() { let app_path = exe_dir.join("donutbrowser.exe"); if app_path.exists() { let _ = Command::new(app_path).spawn(); return; } } } let paths = [ dirs::data_local_dir().map(|p| p.join("Donut Browser").join("Donut Browser.exe")), Some(PathBuf::from( "C:\\Program Files\\Donut Browser\\Donut Browser.exe", )), ]; for path in paths.iter().flatten() { if path.exists() { let _ = Command::new(path).spawn(); return; } } } #[cfg(target_os = "linux")] { let _ = Command::new("donutbrowser").spawn(); } } pub fn activate_gui() { #[cfg(target_os = "macos")] { let _ = Command::new("osascript") .args(["-e", "tell application \"Donut Browser\" to activate"]) .spawn(); } } pub fn quit_gui() { log::info!("[daemon] Quitting GUI..."); #[cfg(target_os = "macos")] { let _ = Command::new("osascript") .args(["-e", "tell application \"Donut Browser\" to quit"]) .output(); } #[cfg(target_os = "windows")] { let _ = Command::new("taskkill") .args(["/IM", "Donut.exe", "/F"]) .output(); let _ = Command::new("taskkill") .args(["/IM", "donutbrowser.exe", "/F"]) .output(); } #[cfg(target_os = "linux")] { let _ = Command::new("pkill").args(["-x", "donutbrowser"]).output(); } } pub fn set_gui_running(running: bool) { GUI_RUNNING.store(running, Ordering::SeqCst); }