初始化提交
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
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
This commit is contained in:
225
crates/openfang-desktop/src/tray.rs
Normal file
225
crates/openfang-desktop/src/tray.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user