commit 7d54197779e5422d353d1013552d9e25573b435c
parent 45f609be411892be090984d17c34d5510c230127
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Wed, 2 Sep 2026 23:00:28 +0200
Add desktop tray background mode
Diffstat:
3 files changed, 93 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
@@ -62,6 +62,10 @@ The deployment script builds Linux CLI archives for x86_64 and aarch64, builds t
- `downloads/iuna-v0.4.9-macos-aarch64-desktop.app.zip`
- `downloads/iuna-v0.4.9-windows-x86_64-desktop-setup.exe`
+On macOS and Windows, closing the desktop window keeps the node running from the menu bar or
+system tray. Choose **Open iuna** to reopen the window, or **Quit iuna** to stop the node. On
+Windows, a left click on the tray icon also reopens the window directly.
+
Release and deploy with:
```sh
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
@@ -10,5 +10,5 @@ publish = false
tauri-build = { version = "2", features = [] }
[dependencies]
-tauri = { version = "2", features = [] }
+tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
@@ -7,6 +7,13 @@ use std::{
use tauri::WindowEvent;
use tauri_plugin_shell::{ShellExt, process::CommandChild};
+#[cfg(any(target_os = "macos", target_os = "windows"))]
+use tauri::{
+ Manager,
+ menu::{Menu, MenuItem},
+ tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
+};
+
struct IunaSidecar(Mutex<Option<CommandChild>>);
struct IunaSleepInhibitor(Mutex<Option<SleepInhibitor>>);
@@ -17,6 +24,9 @@ fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
+ #[cfg(any(target_os = "macos", target_os = "windows"))]
+ setup_desktop_tray(app)?;
+
let (mut events, child) = app
.shell()
.sidecar("iuna-sidecar")
@@ -30,9 +40,19 @@ fn main() {
let sidecar = SIDECAR.get_or_init(|| IunaSidecar(Mutex::new(None)));
*sidecar.0.lock().expect("sidecar mutex poisoned") = Some(child);
+
Ok(())
})
.on_window_event(|_window, event| {
+ #[cfg(any(target_os = "macos", target_os = "windows"))]
+ if let WindowEvent::CloseRequested { api, .. } = event {
+ api.prevent_close();
+ if let Err(error) = _window.hide() {
+ eprintln!("iuna desktop could not hide its window: {error}");
+ }
+ }
+
+ #[cfg(not(any(target_os = "macos", target_os = "windows")))]
if matches!(event, WindowEvent::CloseRequested { .. }) {
stop_sidecar();
}
@@ -40,6 +60,17 @@ fn main() {
.build(tauri::generate_context!())
.expect("error while building iuna desktop")
.run(|_app, event| {
+ #[cfg(target_os = "macos")]
+ if matches!(
+ &event,
+ tauri::RunEvent::Reopen {
+ has_visible_windows: false,
+ ..
+ }
+ ) {
+ show_main_window(_app);
+ }
+
if matches!(
event,
tauri::RunEvent::Exit | tauri::RunEvent::ExitRequested { .. }
@@ -49,6 +80,63 @@ fn main() {
});
}
+#[cfg(any(target_os = "macos", target_os = "windows"))]
+fn setup_desktop_tray(app: &tauri::App) -> tauri::Result<()> {
+ const OPEN_MENU_ID: &str = "open-iuna";
+ const QUIT_MENU_ID: &str = "quit-iuna";
+
+ let open = MenuItem::with_id(app, OPEN_MENU_ID, "Open iuna", true, None::<&str>)?;
+ let quit = MenuItem::with_id(app, QUIT_MENU_ID, "Quit iuna", true, None::<&str>)?;
+ let menu = Menu::with_items(app, &[&open, &quit])?;
+
+ let mut tray = TrayIconBuilder::new()
+ .menu(&menu)
+ .tooltip("iuna — node running")
+ .show_menu_on_left_click(cfg!(target_os = "macos"))
+ .on_menu_event(|app, event| match event.id().as_ref() {
+ OPEN_MENU_ID => show_main_window(app),
+ QUIT_MENU_ID => app.exit(0),
+ _ => {}
+ })
+ .on_tray_icon_event(|tray, event| {
+ if cfg!(target_os = "windows")
+ && matches!(
+ event,
+ TrayIconEvent::Click {
+ button: MouseButton::Left,
+ button_state: MouseButtonState::Up,
+ ..
+ }
+ )
+ {
+ show_main_window(tray.app_handle());
+ }
+ });
+
+ if let Some(icon) = app.default_window_icon() {
+ tray = tray.icon(icon.clone());
+ }
+
+ tray.build(app)?;
+ Ok(())
+}
+
+#[cfg(any(target_os = "macos", target_os = "windows"))]
+fn show_main_window(app: &tauri::AppHandle) {
+ let Some(window) = app.get_webview_window("main") else {
+ eprintln!("iuna desktop could not find its main window");
+ return;
+ };
+
+ if let Err(error) = window
+ .show()
+ .and_then(|_| window.unminimize())
+ .and_then(|_| window.set_focus())
+ {
+ eprintln!("iuna desktop could not show its window: {error}");
+ }
+}
+
fn stop_sidecar() {
if let Some(sidecar) = SIDECAR.get() {
if let Some(child) = sidecar.0.lock().expect("sidecar mutex poisoned").take() {