Files
openclaw/apps/linux/src-tauri/src/tray.rs
Peter Steinberger f460c2871a feat(linux): add signed auto-updater to the Tauri companion app (#108770)
Sparkle-parity self-update for the apps/linux companion: tauri-plugin-updater +
process, checked silently on launch and from a "Check for Updates" tray item /
manual trigger. AppImage installs download, verify the signed update, and wait
for a user-confirmed restart; package-managed (deb) installs get a release-page
notice instead of replacing files the system package manager owns. The
linux-app release workflow signs the AppImage and publishes a signed latest.json
manifest that the app verifies with the committed minisign public key.
2026-07-16 01:48:08 -07:00

161 lines
5.1 KiB
Rust

use crate::gateway::{GatewayAction, GatewaySnapshot};
use crate::DesktopState;
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
use tauri::{App, AppHandle, Manager};
const OPEN_ID: &str = "open-dashboard";
const CHECK_UPDATES_ID: &str = "check-for-updates";
const START_ID: &str = "start-gateway";
const STOP_ID: &str = "stop-gateway";
const RESTART_ID: &str = "restart-gateway";
const QUIT_ID: &str = "quit";
pub struct TrayHandles {
_tray: TrayIcon<tauri::Wry>,
status: MenuItem<tauri::Wry>,
open: MenuItem<tauri::Wry>,
_check_updates: MenuItem<tauri::Wry>,
start: MenuItem<tauri::Wry>,
stop: MenuItem<tauri::Wry>,
restart: MenuItem<tauri::Wry>,
}
impl TrayHandles {
pub fn update(&self, snapshot: &GatewaySnapshot) {
let _ = self
.status
.set_text(format!("Gateway: {}", snapshot.status));
let _ = self.open.set_enabled(true);
let _ = self
.start
.set_enabled(snapshot.installed && !snapshot.running && !snapshot.reachable);
let _ = self
.stop
.set_enabled(snapshot.installed && snapshot.running);
let _ = self.restart.set_enabled(snapshot.installed);
}
}
pub fn build(app: &App, state: DesktopState) -> tauri::Result<TrayHandles> {
let status = MenuItem::with_id(
app,
"gateway-status",
"Gateway: Checking…",
false,
None::<&str>,
)?;
let open = MenuItem::with_id(app, OPEN_ID, "Open Dashboard", true, None::<&str>)?;
let check_updates = MenuItem::with_id(
app,
CHECK_UPDATES_ID,
"Check for Updates",
true,
None::<&str>,
)?;
let start = MenuItem::with_id(app, START_ID, "Start Gateway", false, None::<&str>)?;
let stop = MenuItem::with_id(app, STOP_ID, "Stop Gateway", false, None::<&str>)?;
let restart = MenuItem::with_id(app, RESTART_ID, "Restart Gateway", false, None::<&str>)?;
let quit = MenuItem::with_id(app, QUIT_ID, "Quit OpenClaw", true, None::<&str>)?;
let separator_one = PredefinedMenuItem::separator(app)?;
let separator_two = PredefinedMenuItem::separator(app)?;
let separator_three = PredefinedMenuItem::separator(app)?;
let menu = Menu::with_items(
app,
&[
&status,
&separator_one,
&open,
&check_updates,
&separator_two,
&start,
&stop,
&restart,
&separator_three,
&quit,
],
)?;
let tray_icon = tauri::image::Image::from_bytes(include_bytes!("../icons/32x32.png"))?;
let menu_state = state.clone();
let tray_builder = TrayIconBuilder::with_id("openclaw-main")
.icon(tray_icon)
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(move |app, event| {
handle_menu(app, &menu_state, event.id().as_ref());
})
// Linux tray backends expose the Open action through the menu; Tauri also
// emits this direct click event on platforms that support it.
.on_tray_icon_event(|tray, event| {
if matches!(
event,
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
}
) {
show_window(tray.app_handle());
}
});
#[cfg(target_os = "macos")]
let tray_builder = tray_builder.icon_as_template(true);
let tray = tray_builder.build(app)?;
Ok(TrayHandles {
_tray: tray,
status,
open,
_check_updates: check_updates,
start,
stop,
restart,
})
}
pub fn show_window(app: &AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
fn handle_menu(app: &AppHandle, state: &DesktopState, id: &str) {
match id {
QUIT_ID => {
state.quit();
app.exit(0);
}
OPEN_ID => {
show_window(app);
spawn_connect(app.clone(), state.clone());
}
CHECK_UPDATES_ID => {
show_window(app);
crate::updater::spawn_check(app.clone());
}
START_ID => spawn_action(app.clone(), state.clone(), GatewayAction::Start),
STOP_ID => spawn_action(app.clone(), state.clone(), GatewayAction::Stop),
RESTART_ID => spawn_action(app.clone(), state.clone(), GatewayAction::Restart),
_ => {}
}
}
fn spawn_connect(app: AppHandle, state: DesktopState) {
std::thread::spawn(move || {
if let Err(error) = state.connect_explicit_local(&app) {
state.show_error(&app, &error);
}
});
}
fn spawn_action(app: AppHandle, state: DesktopState, action: GatewayAction) {
std::thread::spawn(move || {
if let Err(error) = state.gateway_action(&app, action) {
state.show_error(&app, &error);
}
});
}