refactor(app): Phase 6 — tray adapter skeleton (Open / Quit) (#1409)#1421
refactor(app): Phase 6 — tray adapter skeleton (Open / Quit) (#1409)#1421shm11C3 wants to merge 1 commit into
Conversation
…only) Closes #1409. Add `src-tauri/src/adapters/tray.rs` so users have a way back to the window once the close-to-background path is active. Phase 6 ships only the bare entry-point: `Open` and `Quit` menu items plus a left-click that restores the main window. Live metric rendering belongs to #1401, and the Pause / Resume entry belongs to #1275. Setup is gated behind `lifecycle::should_close_to_background` (the `HARDVIZ_CLOSE_TO_BACKGROUND=1` env var introduced in Phase 5), so the user-visible behavior stays unchanged from Phase 5: in default release builds, no tray is registered. The persisted setting that flips the close behavior — and the always-on tray that follows from it — ships with #1275. 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` only when `should_close_to_background` is true; failures are logged and the app continues without a tray (the `Quit` command path is still reachable). - `src-tauri/Cargo.toml` — enable the `tray-icon` feature on `tauri = "2.10.3"`. Path note: the #1409 issue body says `src-tauri/src/app/adapters/tray.rs`, but per the parent epic (#1402, 2026-04-30 changelog) the App `app::adapters` namespace was dropped — adapters live directly under `src-tauri/src/adapters/`, next to the existing `window.rs`. Same path-resolution as Phase 5. 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 #1409): live metric values in the tray (#1401), Pause / Resume entries (#1275), custom icons / theming.
📝 WalkthroughWalkthroughThis pull request introduces tray icon functionality to the Tauri application. A new Changes
Sequence DiagramsequenceDiagram
participant User
participant Tray as System Tray
participant TrayAdapter
participant Window as Window Manager
participant Lifecycle
User->>Tray: Left-click tray icon
Tray->>TrayAdapter: Emit "Up" event
TrayAdapter->>Window: Locate "main" window
TrayAdapter->>Window: Show & focus window
User->>Tray: Click "Open" menu item
Tray->>TrayAdapter: Menu event ("Open")
TrayAdapter->>Window: Locate "main" window
TrayAdapter->>Window: Show & focus window
User->>Tray: Click "Quit" menu item
Tray->>TrayAdapter: Menu event ("Quit")
TrayAdapter->>Lifecycle: Spawn request_quit task
Lifecycle->>Lifecycle: Trigger app shutdown
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels
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.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src-tauri/src/workers/mod.rs (1)
29-39:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
terminate_allshould also cleartraystate.
WorkersStatenow ownstray(Line 17), butterminate_alldoes nottake()it. If a shutdown path doesn’t immediately end the process, the tray handle can outlive worker teardown and keep the icon around unexpectedly.Suggested patch
pub async fn terminate_all(&self) { @@ 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(); @@ if let Some(hw_archive) = hw_archive { hw_archive.terminate().await; } + + // Drop the tray handle as part of teardown. + drop(tray); } }🤖 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 29 - 39, terminate_all currently drops monitor, window_adapter and hw_archive but neglects to take and clear the tray field on WorkersState, allowing the tray handle to outlive teardown; update terminate_all to also lock and call take() on self.tray (similar to monitor/window_adapter/hw_archive) so the tray handle is removed/dropped during shutdown, using the same locking pattern and ensuring it happens before returning from terminate_all.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src-tauri/src/workers/mod.rs`:
- Around line 29-39: terminate_all currently drops monitor, window_adapter and
hw_archive but neglects to take and clear the tray field on WorkersState,
allowing the tray handle to outlive teardown; update terminate_all to also lock
and call take() on self.tray (similar to monitor/window_adapter/hw_archive) so
the tray handle is removed/dropped during shutdown, using the same locking
pattern and ensuring it happens before returning from terminate_all.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 887cde8e-6f96-48ab-bc6e-81a006651683
📒 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
There was a problem hiding this comment.
Pull request overview
Adds an App-layer system tray adapter skeleton (Phase 6 of the Core/App split epic) to support “Open” / “Quit” tray interactions when the close-to-background lifecycle path is enabled.
Changes:
- Introduces
TrayAdapterthat builds a tray icon + menu (Open/Quit) and handles left-click restore + async quit. - Persists the tray handle in
WorkersStateso the OS tray icon lifetime matches the process lifetime. - Gates tray registration behind
lifecycle::should_close_to_background()and enables Tauri’stray-iconfeature.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src-tauri/src/adapters/tray.rs |
New tray adapter with menu + click handlers and a minimal restore/quit implementation. |
src-tauri/src/adapters/mod.rs |
Exposes the new tray adapter module. |
src-tauri/src/workers/mod.rs |
Stores TrayAdapter in WorkersState to keep the tray icon alive. |
src-tauri/src/lib.rs |
Registers the tray during app setup when close-to-background is active; logs and continues on failure. |
src-tauri/Cargo.toml |
Enables Tauri tray-icon feature. |
| // Linux desktops without an indicator implementation, or | ||
| // any other tray failure: log and continue. `Quit` from | ||
| // the in-process command path still works, so the user is | ||
| // not stranded — just without a visible tray entry point. | ||
| log_warn!( | ||
| &format!("tray icon setup failed; continuing without tray: {e}"), | ||
| "lib::setup", | ||
| None::<&str> | ||
| ); |
There was a problem hiding this comment.
If should_close_to_background() is true but tray setup fails, the app will still hide the main window on close (see lifecycle::handle_close_request), leaving no guaranteed way to restore the window because no tray icon exists. Consider treating tray availability as part of the gate (e.g., only hide-on-close when a tray was successfully registered; otherwise fall back to quit-on-close or keep the window visible and log a warning).
| // Linux desktops without an indicator implementation, or | |
| // any other tray failure: log and continue. `Quit` from | |
| // the in-process command path still works, so the user is | |
| // not stranded — just without a visible tray entry point. | |
| log_warn!( | |
| &format!("tray icon setup failed; continuing without tray: {e}"), | |
| "lib::setup", | |
| None::<&str> | |
| ); | |
| // Close-to-background requires a working tray entry point. | |
| // If tray registration fails, continuing startup would allow | |
| // the main window to be hidden on close with no guaranteed | |
| // way to restore it. | |
| log_warn!( | |
| &format!( | |
| "tray icon setup failed while close-to-background is enabled; aborting startup: {e}" | |
| ), | |
| "lib::setup", | |
| None::<&str> | |
| ); | |
| return Err(e.into()); |
| //! when that happens [`TrayAdapter::setup`] logs and returns the | ||
| //! error to the caller, who falls back to running without a tray. |
There was a problem hiding this comment.
The module-level docs say that when tray registration fails, TrayAdapter::setup “logs and returns the error to the caller”, but setup currently only returns the error; the logging happens in lib.rs. Please update the docs to match the actual behavior (or move the logging into setup if you want that responsibility there).
| //! when that happens [`TrayAdapter::setup`] logs and returns the | |
| //! error to the caller, who falls back to running without a tray. | |
| //! when that happens [`TrayAdapter::setup`] returns the error to the | |
| //! caller, who logs it and falls back to running without a tray. |
Rust Backend Coverage ReportCoverage Details |
|
Closing without merging. The tray adapter has been rescoped out of the #1402 Core/App split Epic and into a standalone feature issue (#1422), which sits as a prerequisite for #1275. Reasoning: the Epic's acceptance criteria require no user-visible regression, and a tray icon — even a minimal one — is user-visible UX, not refactor work. The branch |
|
Correction: the branch |
Closes #1409. Part of #1402 (Epic).
Summary
src-tauri/src/adapters/tray.rswith a minimalTrayAdapter(Open / Quit menu, left-click-restore).lifecycle::should_close_to_background(HARDVIZ_CLOSE_TO_BACKGROUND=1), so user-visible behavior stays the same as Phase 5.Quitvia the in-process command path keeps working.tray-iconfeature ontauri = "2.10.3".User-visible behavior
Unchanged in default release builds. With
HARDVIZ_CLOSE_TO_BACKGROUND=1:Open(or left-click on Windows / macOS) restores the main window with state intact.Quitexits cleanly via Phase 5'slifecycle::request_quit(state machine →Stopped, archive flushed,app.exit(0)).Path note
The #1409 issue body says
src-tauri/src/app/adapters/tray.rs, but #1402's 2026-04-30 changelog supersedes it ("app::adapters→adapters"). Following the parent epic —src-tauri/src/adapters/tray.rs, next to the existingwindow.rs. Same path-resolution as Phase 5.Visibility gate
Per discussion, the tray is gated by
HARDVIZ_CLOSE_TO_BACKGROUNDrather than always-on:Layout
src-tauri/src/adapters/tray.rs—TrayAdapter::setup(app)builds the menu, wireson_menu_eventandon_tray_icon_event, and returns a struct that owns theTrayIconhandle.src-tauri/src/workers/mod.rs—WorkersStategainstray: Mutex<Option<TrayAdapter>>so the icon survives setup-closure scope.src-tauri/src/lib.rs— registers the tray insetupwhenshould_close_to_background()is true; logs and continues on failure.src-tauri/Cargo.toml— addstray-iconto thetaurifeatures list.Platform notes (in code comments)
show_menu_on_left_click(false)keeps the click event reaching our handler.Tests
cargo test --workspace --lib --binspasses (140 + 153). No new tests — the tray adapter is mostly Tauri-runtime glue and the menu / tray event handlers are exercised through thetauri::Appsetup, which lives outside the unit-test boundary. The pure-Rust dispatch (handle_menu_eventmatches onMENU_OPEN_ID/MENU_QUIT_ID) is small enough to verify by inspection.src/rspc/bindings.tsunchanged.Out of scope (per #1409)
Test plan
cargo tauri-fmt -- --checkcargo tauri-lintcargo test --workspace --lib --bins -- --test-threads=1(theadl_diagnosticexample failure is pre-existing ondevelop)npm run lintnpm run testHARDVIZ_CLOSE_TO_BACKGROUND=1, close the window, confirm the tray icon stays visible. ClickOpenand left-click the icon — both restore the window. ClickQuit— the app exits with a final archive row written.show_menu_on_left_click(false)actually reaches the click handler.Summary by CodeRabbit