feat: daemon support, general improvement, and preparation for Windows release

This commit is contained in:
zhom
2026-02-01 20:55:09 +04:00
parent e9f4edd120
commit 4a59459eb2
58 changed files with 9763 additions and 296 deletions
+94 -15
View File
@@ -80,6 +80,12 @@ pub fn enable_autostart() -> io::Result<()> {
let plist_path = plist_dir.join("com.donutbrowser.daemon.plist");
// Get log directory (use data directory instead of /tmp)
let log_dir = get_data_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("logs");
fs::create_dir_all(&log_dir)?;
let plist_content = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -89,21 +95,29 @@ pub fn enable_autostart() -> io::Result<()> {
<string>com.donutbrowser.daemon</string>
<key>ProgramArguments</key>
<array>
<string>{}</string>
<string>start</string>
<string>{daemon_path}</string>
<string>run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>LimitLoadToSessionType</key>
<string>Aqua</string>
<key>KeepAlive</key>
<false/>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>ProcessType</key>
<string>Interactive</string>
<key>StandardOutPath</key>
<string>/tmp/donut-daemon.out.log</string>
<string>{log_dir}/daemon.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/donut-daemon.err.log</string>
<string>{log_dir}/daemon.err.log</string>
</dict>
</plist>
"#,
daemon_path.display()
daemon_path = daemon_path.display(),
log_dir = log_dir.display()
);
fs::write(&plist_path, plist_content)?;
@@ -112,13 +126,19 @@ pub fn enable_autostart() -> io::Result<()> {
Ok(())
}
#[cfg(target_os = "macos")]
pub fn get_plist_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join("Library/LaunchAgents/com.donutbrowser.daemon.plist"))
}
#[cfg(target_os = "macos")]
pub fn disable_autostart() -> io::Result<()> {
let plist_path = dirs::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))?
.join("Library/LaunchAgents/com.donutbrowser.daemon.plist");
let plist_path = get_plist_path()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))?;
if plist_path.exists() {
// First unload the launch agent if it's loaded
let _ = unload_launch_agent();
fs::remove_file(&plist_path)?;
log::info!("Removed launch agent at {:?}", plist_path);
}
@@ -128,12 +148,71 @@ pub fn disable_autostart() -> io::Result<()> {
#[cfg(target_os = "macos")]
pub fn is_autostart_enabled() -> bool {
dirs::home_dir()
.map(|h| {
h.join("Library/LaunchAgents/com.donutbrowser.daemon.plist")
.exists()
})
.unwrap_or(false)
get_plist_path().is_some_and(|p| p.exists())
}
#[cfg(target_os = "macos")]
pub fn load_launch_agent() -> io::Result<()> {
use std::process::Command;
let plist_path = get_plist_path()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not determine plist path"))?;
if !plist_path.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"Launch agent plist does not exist",
));
}
// Use launchctl load to start the daemon via launchd
// The -w flag writes the "disabled" key to the override plist
let output = Command::new("launchctl")
.args(["load", "-w"])
.arg(&plist_path)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
// "already loaded" is not an error condition for us
if !stderr.contains("already loaded") {
return Err(io::Error::other(format!(
"launchctl load failed: {}",
stderr
)));
}
}
log::info!("Loaded launch agent via launchctl");
Ok(())
}
#[cfg(target_os = "macos")]
pub fn unload_launch_agent() -> io::Result<()> {
use std::process::Command;
let plist_path = get_plist_path()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not determine plist path"))?;
if !plist_path.exists() {
return Ok(());
}
let output = Command::new("launchctl")
.args(["unload"])
.arg(&plist_path)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
// Not being loaded is not an error
if !stderr.contains("Could not find specified service") {
log::warn!("launchctl unload warning: {}", stderr);
}
}
log::info!("Unloaded launch agent via launchctl");
Ok(())
}
#[cfg(target_os = "linux")]
+11 -5
View File
@@ -6,7 +6,9 @@ use tray_icon::{Icon, TrayIcon, TrayIconBuilder};
static GUI_RUNNING: AtomicBool = AtomicBool::new(false);
pub fn load_icon() -> Icon {
let icon_bytes = include_bytes!("../../icons/32x32.png");
// Use the generated template icon (44x44 for retina, macOS standard menu bar size)
// This is the donut logo converted to template format (black with alpha)
let icon_bytes = include_bytes!("../../icons/tray-icon-44.png");
let image = image::load_from_memory(icon_bytes)
.expect("Failed to load icon")
@@ -89,12 +91,16 @@ impl TrayMenu {
}
pub fn create_tray_icon(icon: Icon, menu: &Menu) -> TrayIcon {
TrayIconBuilder::new()
let builder = TrayIconBuilder::new()
.with_icon(icon)
.with_tooltip("Donut Browser")
.with_menu(Box::new(menu.clone()))
.build()
.expect("Failed to create tray icon")
.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() {