feat(window): minimize to system tray on close and enforce single-instance focus#397
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds tray and single-instance support to the Tauri app. The manifest enables ChangesTray launch and shutdown coordination
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant Tauri app
participant tauri_plugin_single_instance
participant main webview window
participant tray menu
Tauri app->>tauri_plugin_single_instance: register single-instance handler
User->>Tauri app: launch a second instance
tauri_plugin_single_instance->>main webview window: show, unminimize, focus
User->>tray menu: select Show Termul
Tauri app->>main webview window: show, unminimize, focus
User->>main webview window: click close
Tauri app->>main webview window: hide unless quitting
User->>tray menu: select Quit Termul
Tauri app->>Tauri app: set IS_QUITTING and exit
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/src/lib.rs`:
- Around line 894-895: The ExitRequested cleanup flow only tracks completion
with CLEANUP_DONE, so a second request can start another cleanup before the
first task marks it done. Add a separate in-progress atomic guard in the same
area as IS_QUITTING and CLEANUP_DONE, set it immediately when entering the async
cleanup path in the ExitRequested handler, and use it to short-circuit any
subsequent requests until the task finishes. Keep the existing
CLEANUP_DONE.store(true, ...) calls at the end of each cleanup task, and update
the cleanup-start checks in the referenced cleanup blocks so kill_all(),
destroy_all(), and exit(0) can only run once.
- Around line 934-943: The window-restore flow currently focuses before
restoring, which can leave the app visible but not foregrounded on some
platforms. Update the single-instance handler in
builder.plugin(tauri_plugin_single_instance::init(...)) and the tray action
paths that use window.show()/set_focus()/unminimize() so that unminimize()
happens before set_focus(), keeping the same behavior elsewhere.
- Around line 1136-1148: The window close handler in lib.rs currently prevents
close and hides the main window for every platform, which conflicts with the
Windows shutdown requirement. Update the CloseRequested handling around
app.get_webview_window("main"), window.on_window_event, and IS_QUITTING so the
hide-on-close path is only used on non-Windows platforms, while Windows allows
the app to fully exit when the user closes the window; if needed, align the
acceptance criteria with this platform-specific behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0e479205-6669-4e3d-8bbb-21ff20ac3386
⛔ Files ignored due to path filters (1)
src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
src-tauri/Cargo.tomlsrc-tauri/src/lib.rs
| static IS_QUITTING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); | ||
| static CLEANUP_DONE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Guard cleanup while it is in progress, not only after it finishes.
CLEANUP_DONE is set inside the async cleanup task. If another ExitRequested arrives before that store, this code starts another cleanup task and may run kill_all(), destroy_all(), and exit(0) multiple times. Add a separate in-progress guard.
Proposed fix
static IS_QUITTING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
+static CLEANUP_IN_PROGRESS: std::sync::atomic::AtomicBool =
+ std::sync::atomic::AtomicBool::new(false);
static CLEANUP_DONE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); if let RunEvent::ExitRequested { api, .. } = event {
if CLEANUP_DONE.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
// Prevent the default exit behavior so we can cleanup first
api.prevent_exit();
+ if CLEANUP_IN_PROGRESS.swap(true, std::sync::atomic::Ordering::SeqCst) {
+ return;
+ }Keep the existing CLEANUP_DONE.store(true, ...) calls at the end of each cleanup task.
Also applies to: 1315-1319, 1356-1383
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/src/lib.rs` around lines 894 - 895, The ExitRequested cleanup flow
only tracks completion with CLEANUP_DONE, so a second request can start another
cleanup before the first task marks it done. Add a separate in-progress atomic
guard in the same area as IS_QUITTING and CLEANUP_DONE, set it immediately when
entering the async cleanup path in the ExitRequested handler, and use it to
short-circuit any subsequent requests until the task finishes. Keep the existing
CLEANUP_DONE.store(true, ...) calls at the end of each cleanup task, and update
the cleanup-start checks in the referenced cleanup blocks so kill_all(),
destroy_all(), and exit(0) can only run once.
| // Single-instance guard: jika user buka termul lagi padahal sudah jalan, | ||
| // cukup fokuskan window yang ada — jangan spawn instance baru. | ||
| builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { | ||
| if let Some(window) = app.get_webview_window("main") { | ||
| let _ = window.show(); | ||
| let _ = window.set_focus(); | ||
| let _ = window.unminimize(); | ||
| } | ||
| })); | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -euo pipefail
# Map relevant Rust files and docs first
git ls-files 'src-tauri/src/lib.rs' 'docs/architecture.md' 'docs/api-contracts.md' 'docs/source-tree-analysis.md'
# Inspect the relevant code ranges around the cited lines and tray handlers
sed -n '900,965p' src-tauri/src/lib.rs
printf '\n---\n'
sed -n '1088,1145p' src-tauri/src/lib.rs
# Check for other focus/restore usages
rg -n "set_focus\\(|unminimize\\(|show\\(" src-tauri/src/lib.rs
# Read the required Tauri docs if present
for f in docs/architecture.md docs/api-contracts.md docs/source-tree-analysis.md; do
if [ -f "$f" ]; then
printf '\n=== %s ===\n' "$f"
sed -n '1,220p' "$f"
fi
doneRepository: gnoviawan/termul
Length of output: 27745
🌐 Web query:
Tauri 2 window unminimize set_focus order minimized window focus restore platform docs
💡 Result:
In Tauri 2, calling set_focus on a window that is currently minimized will not automatically restore it to a visible state [1][2]. To successfully bring a minimized window to the front and ensure it is focused, you must explicitly unminimize the window before or in sequence with the focus command [1][2]. Recommended Approach To restore and focus a window reliably across platforms, follow this operational order [2][3]: 1. Unminimize: Call the unminimize method [1][2]. 2. Focus: Call the set_focus method [1][2]. If you encounter issues where this sequence is insufficient—particularly on certain Linux desktop environments—some developers successfully use a workaround by hiding the window, unminimizing it, showing it, and then focusing it [3]: hide -> unminimize -> show -> set_focus [3] API Reference Both the JavaScript API and the Rust backend provide these methods for window management: - JavaScript: Use window.unminimize and window.setFocus [4][5]. - Rust: Use window.unminimize and window.set_focus [6][7]. Note that behavior can depend on the underlying operating system's window manager, which Tauri does not control directly [3]. Always ensure that the window has the necessary permissions defined in your application's capabilities configuration to perform these actions [8].
Citations:
- 1: [bug] setFocus not unminimizing the window tauri-apps/tauri#5212
- 2: [bug] Cant bring existing window to front tauri-apps/tauri#8361
- 3: [bug] window.unminimize() not working on Linux tauri-apps/tauri#5974
- 4: https://v2.tauri.app/reference/javascript/api/namespacewindow/
- 5: https://github.com/tauri-apps/tauri/blob/dev/packages/api/src/window.ts
- 6: https://docs.rs/tauri/latest/tauri/window/struct.Window.html
- 7: https://docs.rs/tauri/2.1.1/tauri/window/struct.Window.html
- 8: https://v2.tauri.app/learn/window-customization/
Restore before focusing. Move set_focus() after unminimize() in the single-instance, tray “Show”, and left-click paths; focusing first can leave a minimized window restored but not foregrounded on some platforms.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/src/lib.rs` around lines 934 - 943, The window-restore flow
currently focuses before restoring, which can leave the app visible but not
foregrounded on some platforms. Update the single-instance handler in
builder.plugin(tauri_plugin_single_instance::init(...)) and the tray action
paths that use window.show()/set_focus()/unminimize() so that unminimize()
happens before set_focus(), keeping the same behavior elsewhere.
| // Intercept window close (X button) → sembunyikan ke tray, | ||
| // bukan terminate proses. User bisa quit lewat menu tray. | ||
| if let Some(window) = app.get_webview_window("main") { | ||
| let app_handle_for_close = handle.clone(); | ||
| window.on_window_event(move |event| { | ||
| if let tauri::WindowEvent::CloseRequested { api, .. } = event { | ||
| if IS_QUITTING.load(std::sync::atomic::Ordering::SeqCst) { | ||
| // Allow close to proceed and terminate the app | ||
| } else { | ||
| api.prevent_close(); | ||
| if let Some(w) = app_handle_for_close.get_webview_window("main") { | ||
| let _ = w.hide(); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Don’t hide-on-close on Windows if #390 requires shutdown.
This handler prevents close and hides the window on every OS. That satisfies the macOS background behavior, but conflicts with the stated Windows objective: closing should not leave the app running in Task Manager. Scope hide-on-close away from Windows, or explicitly update the acceptance criteria.
Possible platform-scoped fix
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if IS_QUITTING.load(std::sync::atomic::Ordering::SeqCst) {
// Allow close to proceed and terminate the app
} else {
+ #[cfg(target_os = "windows")]
+ {
+ api.prevent_close();
+ IS_QUITTING.store(true, std::sync::atomic::Ordering::SeqCst);
+ app_handle_for_close.exit(0);
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ {
- api.prevent_close();
- if let Some(w) = app_handle_for_close.get_webview_window("main") {
- let _ = w.hide();
+ api.prevent_close();
+ if let Some(w) = app_handle_for_close.get_webview_window("main") {
+ let _ = w.hide();
+ }
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Intercept window close (X button) → sembunyikan ke tray, | |
| // bukan terminate proses. User bisa quit lewat menu tray. | |
| if let Some(window) = app.get_webview_window("main") { | |
| let app_handle_for_close = handle.clone(); | |
| window.on_window_event(move |event| { | |
| if let tauri::WindowEvent::CloseRequested { api, .. } = event { | |
| if IS_QUITTING.load(std::sync::atomic::Ordering::SeqCst) { | |
| // Allow close to proceed and terminate the app | |
| } else { | |
| api.prevent_close(); | |
| if let Some(w) = app_handle_for_close.get_webview_window("main") { | |
| let _ = w.hide(); | |
| } | |
| // Intercept window close (X button) → sembunyikan ke tray, | |
| // bukan terminate proses. User bisa quit lewat menu tray. | |
| if let Some(window) = app.get_webview_window("main") { | |
| let app_handle_for_close = handle.clone(); | |
| window.on_window_event(move |event| { | |
| if let tauri::WindowEvent::CloseRequested { api, .. } = event { | |
| if IS_QUITTING.load(std::sync::atomic::Ordering::SeqCst) { | |
| // Allow close to proceed and terminate the app | |
| } else { | |
| #[cfg(target_os = "windows")] | |
| { | |
| api.prevent_close(); | |
| IS_QUITTING.store(true, std::sync::atomic::Ordering::SeqCst); | |
| app_handle_for_close.exit(0); | |
| } | |
| #[cfg(not(target_os = "windows"))] | |
| { | |
| api.prevent_close(); | |
| if let Some(w) = app_handle_for_close.get_webview_window("main") { | |
| let _ = w.hide(); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/src/lib.rs` around lines 1136 - 1148, The window close handler in
lib.rs currently prevents close and hides the main window for every platform,
which conflicts with the Windows shutdown requirement. Update the CloseRequested
handling around app.get_webview_window("main"), window.on_window_event, and
IS_QUITTING so the hide-on-close path is only used on non-Windows platforms,
while Windows allows the app to fully exit when the user closes the window; if
needed, align the acceptance criteria with this platform-specific behavior.
The single-element array form ['class'] is a type error under tailwindcss v4 (DarkModeStrategy expects ['class', string]); the string form 'class' is valid in both v3 and v4. Syncs the config with dev (v4) and pre-resolves the merge conflict when this branch lands on dev.
…avior - Add CLEANUP_IN_PROGRESS guard (compare_exchange) at the top of the ExitRequested handler so a re-entrant exit request cannot spawn a duplicate cleanup task racing kill_all()/destroy_all()/exit(0). CLEANUP_DONE still marks final completion. - Restore windows with show() -> unminimize() -> set_focus() in the single-instance, tray Show, and tray click paths so the restored window is reliably foregrounded on every platform. - Gate hide-on-close to non-Windows; on Windows let the window close so the app exits cleanly (issue gnoviawan#390: no lingering process in Task Manager) and does not block OS shutdown/logoff (WM_QUERYENDSESSION). macOS/Linux still hide to the tray (issue gnoviawan#235).
Implements system tray integration with Show and Quit context menu items, intercepts window CloseRequested events to hide the window, and integrates tauri-plugin-single-instance to focus the existing application instance. Closes #235, Closes #390.
Summary by CodeRabbit
New Features
Bug Fixes