feat(app): system tray adapter with Open / Quit menu (#1422)#1425
Conversation
Closes #1422. Add `src-tauri/src/adapters/tray.rs` so users have a way back to the main window once the close-to-tray setting (#1423) ships. This issue delivers the bare entry-point — `Open` and `Quit` menu items plus a left-click that restores the main window — and stays policy-free: the tray is registered unconditionally on startup, and any decision about *when* it should be visible is owned by #1423. Layout - `src-tauri/src/adapters/tray.rs` — `TrayAdapter::setup(app)` builds a two-item `tauri::menu::Menu`, attaches it to `TrayIconBuilder`, and wires `on_menu_event` / `on_tray_icon_event`. The handle is held in the adapter so dropping the value removes the icon. - `src-tauri/src/workers/mod.rs` — `WorkersState` gains a `tray: Mutex<Option<TrayAdapter>>` slot keeping the icon alive for the process lifetime. - `src-tauri/src/lib.rs` — registers the tray inside `setup`. On failure (typically a Linux desktop without an indicator implementation) we log and continue without a tray; the close-to-tray fallback that prevents users from being stranded ships with #1423. - `src-tauri/Cargo.toml` — enable the `tray-icon` feature on `tauri = "2.10.3"`. Background This is the work originally drafted as Phase 6 (#1409) of the Core/App split Epic (#1402). The Epic's acceptance criteria require no user-visible regression, so the tray (a user-visible UX surface) was rescoped out into a standalone feature issue under the #1275 umbrella. The implementation kept here drops the `HARDVIZ_CLOSE_TO_BACKGROUND` env-var gate that the Phase 6 draft used to stay invisible — visibility is no longer this issue's concern. Platform notes - Windows / macOS: left-click restores; right-click opens the menu. `show_menu_on_left_click(false)` overrides the macOS default of popping the menu on left-click so the click event reaches our handler. - Linux: tray support depends on the desktop environment. AppIndicator-based environments (GNOME with the AppIndicator extension, KDE) typically forward all clicks to the menu and never raise a raw click event, so the left-click-restores path may be silently inactive — `Open` through the menu still works. Environments without an indicator implementation may fail to register the tray at all; the setup error is logged and the app continues without a tray rather than aborting. Out of scope (per #1422) - Live metric values in the tray (#1401). - Pause / Resume entries (#1424). - Visibility policy / persisted close-to-tray setting (#1423). - Custom icons / theming.
📝 WalkthroughWalkthroughThis pull request introduces a system tray adapter for the Tauri application. It enables the Changes
Sequence DiagramsequenceDiagram
participant App as App Startup
participant TraySetup as TrayAdapter::setup
participant OS as OS Tray System
participant State as WorkersState
participant MenuHandler as Menu Event Handler
participant Window as Main Window
participant Lifecycle as lifecycle::request_quit
App->>TraySetup: initialize tray on startup
TraySetup->>OS: register tray icon with menu (Open, Quit)
OS-->>TraySetup: tray registered
TraySetup->>MenuHandler: wire Open handler
TraySetup->>MenuHandler: wire Quit handler
TraySetup->>OS: set left-click to restore window
TraySetup-->>App: TrayAdapter instance
App->>State: store TrayAdapter in Mutex<Option>
par User Interaction
OS->>MenuHandler: user clicks "Open" menu item
MenuHandler->>Window: restore main window (show, unminimize, focus)
and
OS->>MenuHandler: user clicks "Quit" menu item
MenuHandler->>Lifecycle: request_quit() async task
Lifecycle-->>App: shutdown signal
and
OS->>MenuHandler: user left-clicks tray icon
MenuHandler->>Window: restore main window
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 4/5 reviews remaining, refill in 12 minutes. Comment |
There was a problem hiding this comment.
Pull request overview
Adds an App-side Tauri system tray adapter to support background-mode UX under the #1275 umbrella, providing a minimal entry point to restore the window and to quit cleanly via the Phase 5 lifecycle state machine.
Changes:
- Introduces
adapters::tray::TrayAdapterwithOpen/Quitmenu items and left-click restore behavior. - Keeps the tray icon alive for the process lifetime by storing it in
WorkersState. - Registers the tray during Tauri
setup, logging and continuing if tray registration fails (notably on some Linux environments).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src-tauri/src/adapters/tray.rs | New tray adapter wiring menu + click handlers to restore window and request quit. |
| src-tauri/src/adapters/mod.rs | Exposes the new tray adapter module. |
| src-tauri/src/workers/mod.rs | Persists the tray handle in app state so the icon isn’t dropped after setup. |
| src-tauri/src/lib.rs | Registers the tray at startup and logs non-fatal setup failures. |
| src-tauri/Cargo.toml | Enables Tauri’s tray-icon feature to compile tray support. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src-tauri/src/adapters/tray.rs (1)
80-106: ⚡ Quick winExtract event-routing helpers and add inline tests for them.
handle_menu_eventand click filtering are good candidates for pure helper extraction so they can be unit-tested without UI plumbing (e.g., menu-id → action, click-event → restore decision).As per coding guidelines: "Place tests in inline
#[cfg(test)] mod tests { ... }at the bottom of each Rust file. Prefer testing pure functions and extract platform-dependent logic into pure helpers where practical."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src-tauri/src/adapters/tray.rs` around lines 80 - 106, Extract pure helpers so menu routing and click filtering can be unit-tested: create a small pure function (e.g., route_menu_action(id: &str) -> MenuAction enum or Result) that maps MENU_OPEN_ID -> Open, MENU_QUIT_ID -> Quit, others -> Unhandled, then refactor handle_menu_event to call route_menu_action and dispatch to restore_main_window or spawn_quit based on the returned action; similarly add is_restore_click(event: &TrayIconEvent) -> bool that returns true only when event matches TrayIconEvent::Click with MouseButton::Left and MouseButtonState::Up and use it inside handle_tray_event to decide calling restore_main_window; finally add #[cfg(test)] mod tests at the bottom of the file with unit tests for route_menu_action (cover open, quit, unknown) and is_restore_click (cover matching click and non-matching variants).src-tauri/src/workers/mod.rs (1)
13-17: ⚡ Quick winDrop tray state in
terminate_allto keep teardown complete.Now that tray lifetime is owned by
WorkersState, it should also be explicitly released during shutdown alongside other workers.Suggested diff
let monitor = self.monitor.lock().unwrap().take(); let window_adapter = self.window_adapter.lock().unwrap().take(); let hw_archive = self.hw_archive.lock().unwrap().take(); + let _tray = self.tray.lock().unwrap().take();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src-tauri/src/workers/mod.rs` around lines 13 - 17, WorkersState currently owns the tray (pub tray: Mutex<Option<TrayAdapter>>) but terminate_all doesn't drop it, so modify the terminate_all method to explicitly release the tray by locking WorkersState::tray, calling take() (or replace with None) to drop the TrayAdapter and thus remove the OS icon, and handle/ignore PoisonError as appropriate; do this alongside other worker shutdown steps in terminate_all so teardown is complete.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src-tauri/src/adapters/tray.rs`:
- Around line 80-106: Extract pure helpers so menu routing and click filtering
can be unit-tested: create a small pure function (e.g., route_menu_action(id:
&str) -> MenuAction enum or Result) that maps MENU_OPEN_ID -> Open, MENU_QUIT_ID
-> Quit, others -> Unhandled, then refactor handle_menu_event to call
route_menu_action and dispatch to restore_main_window or spawn_quit based on the
returned action; similarly add is_restore_click(event: &TrayIconEvent) -> bool
that returns true only when event matches TrayIconEvent::Click with
MouseButton::Left and MouseButtonState::Up and use it inside handle_tray_event
to decide calling restore_main_window; finally add #[cfg(test)] mod tests at the
bottom of the file with unit tests for route_menu_action (cover open, quit,
unknown) and is_restore_click (cover matching click and non-matching variants).
In `@src-tauri/src/workers/mod.rs`:
- Around line 13-17: WorkersState currently owns the tray (pub tray:
Mutex<Option<TrayAdapter>>) but terminate_all doesn't drop it, so modify the
terminate_all method to explicitly release the tray by locking
WorkersState::tray, calling take() (or replace with None) to drop the
TrayAdapter and thus remove the OS icon, and handle/ignore PoisonError as
appropriate; do this alongside other worker shutdown steps in terminate_all so
teardown is complete.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 22388d69-e913-40a4-beb1-47e0635e6dfc
📒 Files selected for processing (5)
src-tauri/Cargo.tomlsrc-tauri/src/adapters/mod.rssrc-tauri/src/adapters/tray.rssrc-tauri/src/lib.rssrc-tauri/src/workers/mod.rs
Rust Backend Coverage ReportCoverage Details |
Closes #1422. Part of #1275 (umbrella).
Summary
Add a minimal
adapters::tray::TrayAdapterwithOpen/Quitmenu items and a left-click that restores the main window. Callslifecycle::request_quit(shipped in #1420 as Phase 5 of #1402) to drive the lifecycle state machine toStoppedon quit.The tray is registered unconditionally on app startup. The visibility policy — always-on vs gated by a persisted setting — is owned by sibling issue #1423 and lives outside this adapter.
Background
This implementation reuses the work originally drafted as Phase 6 (#1409) of the Core/App split Epic (#1402). The Epic's acceptance criteria require no user-visible regression, so the tray (a user-visible UX surface) was rescoped out of #1402 into the standalone feature issue #1422 under the #1275 umbrella. The implementation kept here drops the
HARDVIZ_CLOSE_TO_BACKGROUNDenv-var gate that the Phase 6 draft used to stay invisible — visibility is no longer this issue's concern.The original Phase 6 PR #1421 was closed without merging; this PR replaces it.
Scope
src-tauri/src/adapters/tray.rs—TrayAdapter::setup(app)builds a two-item menu, attaches it toTrayIconBuilder, wireson_menu_eventandon_tray_icon_event. The handle is held in the adapter so dropping the value removes the icon.src-tauri/src/workers/mod.rs—WorkersStategains atray: Mutex<Option<TrayAdapter>>slot keeping the icon alive for the process lifetime.src-tauri/src/lib.rs— registers the tray insidesetup. On failure (typically a Linux desktop without an indicator implementation) we log and continue without a tray; the close-to-tray fallback that prevents users from being stranded ships with feat: close-to-tray setting + first-run dialog #1423.src-tauri/Cargo.toml— enable thetray-iconfeature ontauri = "2.10.3".Platform notes
show_menu_on_left_click(false)overrides the macOS default of popping the menu on left-click so the click event reaches our handler.Openthrough the menu still works. Environments without an indicator implementation may fail to register the tray at all; the setup error is logged and the app continues without a tray rather than aborting.Out of scope (per #1422)
Test plan
cargo tauri-fmt -- --checkcargo tauri-lintcargo test --workspace --lib --bins -- --test-threads=1(hwviz-core153 +hardware_visualizer140)npm run lintnpm run testwindow.hide(); Quit flushes the archive (final summary written) and exits.Summary by CodeRabbit
Release Notes