mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-01 19:05:31 +02:00
199 lines
4.7 KiB
Rust
199 lines
4.7 KiB
Rust
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<u32> {
|
|
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);
|
|
}
|