Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
226 lines
7.9 KiB
Rust
226 lines
7.9 KiB
Rust
//! 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<dyn std::error::Error>> {
|
|
// 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::<crate::KernelState>() {
|
|
ks.kernel.registry.list().len()
|
|
} else {
|
|
0
|
|
};
|
|
let uptime = if let Some(ks) = app.try_state::<crate::KernelState>() {
|
|
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::<crate::PortState>() {
|
|
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(())
|
|
}
|