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(); } } fn read_gui_pid() -> Option { let path = super::autostart::get_data_dir()?.join("daemon-state.json"); let content = std::fs::read_to_string(path).ok()?; let val: serde_json::Value = serde_json::from_str(&content).ok()?; val.get("gui_pid")?.as_u64().map(|p| p as u32) } fn kill_gui_by_pid() -> bool { let Some(pid) = read_gui_pid() else { return false; }; #[cfg(unix)] { let ret = unsafe { libc::kill(pid as i32, libc::SIGTERM) }; ret == 0 } #[cfg(windows)] { Command::new("taskkill") .args(["/PID", &pid.to_string(), "/F"]) .output() .map(|o| o.status.success()) .unwrap_or(false) } #[cfg(not(any(unix, windows)))] { false } } pub fn quit_gui() { log::info!("[daemon] Quitting GUI..."); if kill_gui_by_pid() { log::info!("[daemon] GUI killed by PID"); return; } log::info!("[daemon] PID-based kill failed, falling back to name-based kill"); #[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); }