From 2713e0204e605fc877ec14cf676387f8bd9e4bd1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Lapersonne Date: Mon, 4 May 2026 16:14:44 +0200 Subject: [PATCH] fix: windows management for macOS (stephanebouget/github-security-alerts#17) The management of the windows for macOS did not work. Several things made the windows disappear: change of spaces / workspaces, management of focus, etc. When run of the app, the browser took the prioirity over the app and displayed the PAT view instead of the app. In addition, the management of a new run with defined token did not display the windows. Here are the fixes with this commit: - consider the app as a try app, i.e. not linked to a space / workspace - management of focus hidding or not the windows was changed - when swipe between spaces, the window is still here - when use of Mission Control, the window is stil here - when outside click, window is still here - if PAT defined at start, display the alerts window In few words, for macOS, app acts like a tray app. This is relevant with its dimensions. Maybe this commit can solve stephanebouget/github-security-alerts#18 and stephanebouget/github-security-alerts#27 Closes stephanebouget/github-security-alerts#17 Assisted-by: Claude Sonnet 4.6 (OpenCode, LLMProxy) Signed-off-by: Pierre-Yves Lapersonne --- package.json | 2 +- src-tauri/Cargo.lock | 66 +++++++++++++++++ src-tauri/Cargo.toml | 3 + src-tauri/src/main.rs | 51 ++++---------- src-tauri/src/state.rs | 4 -- src-tauri/src/window.rs | 144 +++++++------------------------------- src-tauri/tauri.conf.json | 3 +- 7 files changed, 111 insertions(+), 162 deletions(-) diff --git a/package.json b/package.json index d5cd596..1d6181e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "npm run tauri:serve", - "web:serve": "ng serve -o", + "web:serve": "ng serve", "web:build": "ng build --base-href ./", "web:dev": "npm run web:build", "web:prod": "npm run web:build -- -c production", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 03be157..5ab0dbd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "dirs 6.0.0", "dotenv", "image", + "objc2-app-kit", "open", "reqwest 0.12.28", "serde", @@ -2871,8 +2872,38 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.0", "block2", + "libc", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.11.0", + "objc2", "objc2-foundation", ] @@ -2900,6 +2931,41 @@ dependencies = [ "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + [[package]] name = "objc2-encode" version = "4.1.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 596a062..2b16292 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,6 +33,9 @@ tauri-plugin-single-instance = "^2.0" tauri-plugin-autostart = "^2.0" urlencoding = "^2.1" +[target.'cfg(target_os = "macos")'.dependencies] +objc2-app-kit = "0.3.2" + [features] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7e50bea..68ad02d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -37,7 +37,7 @@ mod updater; use config::load_config; use state::AppState; use tray::generate_tray_icon; -use window::{position_window_near_tray, handle_window_focus_lost, handle_window_show}; +use window::{position_window_near_tray, set_macos_window_level}; fn main() { // Load environment variables from .env file @@ -65,12 +65,14 @@ fn main() { )) .manage(AppState { alert_count: Mutex::new(0), - last_shown: Mutex::new(None), - last_focus_lost: Mutex::new(None), - auto_hide_paused: Mutex::new(false), config: Mutex::new(config), }) .setup(|app| { + // On macOS, use Accessory policy so the window floats over all Spaces + // and is not bound to a single Space like a regular app window. + #[cfg(target_os = "macos")] + app.set_activation_policy(tauri::ActivationPolicy::Accessory); + // Enable autostart on first run use tauri_plugin_autostart::ManagerExt; let autostart_manager = app.autolaunch(); @@ -96,26 +98,13 @@ fn main() { let icon_data = generate_tray_icon(None, has_repos); let icon = Image::from_bytes(&icon_data)?; - // Check if user is authenticated - let is_authenticated = { - let state = app.state::(); - let config = state.config.lock().unwrap(); - config.access_token.as_ref() - .map(|t| !t.trim().is_empty()) - .unwrap_or(false) - }; - - // Show window if not authenticated, hide if already logged in + // Always show the window on startup — Angular handles the correct + // view (login vs alerts) based on auth status via checkAuthStatus() if let Some(window) = app.get_webview_window("main") { - if is_authenticated { - let _ = window.hide(); - } else { - // First time - show window for login - handle_window_show(app.handle()); - position_window_near_tray(&window); - let _ = window.show(); - let _ = window.set_focus(); - } + set_macos_window_level(&window); + position_window_near_tray(&window); + let _ = window.show(); + let _ = window.set_focus(); } @@ -165,7 +154,7 @@ fn main() { if window.is_visible().unwrap_or(false) { let _ = window.hide(); } else { - handle_window_show(&app); + set_macos_window_level(&window); position_window_near_tray(&window); let _ = window.show(); let _ = window.set_focus(); @@ -179,21 +168,11 @@ fn main() { Ok(()) }) .on_window_event(|window, event| { + // Intercept close — hide instead of quit (tray app) if let tauri::WindowEvent::CloseRequested { api, .. } = event { let _ = window.hide(); api.prevent_close(); } - if let tauri::WindowEvent::Focused(focused) = event { - if !*focused { - handle_window_focus_lost(window); - } else { - // Window regained focus - clear the focus lost timestamp - if let Some(state) = window.app_handle().try_state::() { - let mut last_focus_lost = state.last_focus_lost.lock().unwrap(); - *last_focus_lost = None; - } - } - } }) .invoke_handler(tauri::generate_handler![ auth::set_token, @@ -209,8 +188,6 @@ fn main() { alerts::get_github_security_alerts, tray::update_tray_icon, system::open_taskbar_settings, - window::pause_auto_hide, - window::resume_auto_hide, updater::check_for_updates, updater::install_update, updater::get_current_version, diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index d8c4afb..0c81cac 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -10,13 +10,9 @@ */ use std::sync::Mutex; -use std::time::Instant; use crate::models::AppConfig; pub struct AppState { pub alert_count: Mutex, - pub last_shown: Mutex>, - pub last_focus_lost: Mutex>, - pub auto_hide_paused: Mutex, pub config: Mutex, } \ No newline at end of file diff --git a/src-tauri/src/window.rs b/src-tauri/src/window.rs index 26e2c41..39d39cd 100644 --- a/src-tauri/src/window.rs +++ b/src-tauri/src/window.rs @@ -9,9 +9,7 @@ * Software description: A modern desktop application that monitors security vulnerabilities across your GitHub repositories in real-time. */ -use tauri::{PhysicalPosition, Manager, LogicalSize}; -use std::time::Instant; -use crate::state::AppState; +use tauri::{PhysicalPosition, LogicalSize}; // ============================================================================ // Window Management @@ -51,125 +49,33 @@ pub fn position_window_near_tray(window: &tauri::WebviewWindow) { } } -pub fn handle_window_focus_lost(window: &tauri::Window) { - let app = window.app_handle(); - - // Check if auto-hide is paused (for dropdown interactions) - let is_paused = if let Some(state) = app.try_state::() { - let auto_hide_paused = state.auto_hide_paused.lock().unwrap(); - *auto_hide_paused - } else { - false - }; - - if is_paused { - println!("[WINDOW] Auto-hide paused - ignoring focus loss"); - return; - } - - // On Linux, use a delayed hide approach to handle dropdown interactions - #[cfg(target_os = "linux")] - { - let should_hide = if let Some(state) = app.try_state::() { - let last_shown = state.last_shown.lock().unwrap(); - if let Some(instant) = *last_shown { - instant.elapsed().as_millis() > 1000 // 1 second minimum on Linux - } else { - true - } - } else { - true - }; - - if should_hide { - let window_clone = window.clone(); - let app_clone = app.clone(); - - // Store the focus lost time - if let Some(state) = app.try_state::() { - let mut last_focus_lost = state.last_focus_lost.lock().unwrap(); - *last_focus_lost = Some(Instant::now()); - } - - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(300)); // Wait 300ms - - // Check if auto-hide is still not paused and focus wasn't regained - let should_still_hide = if let Some(state) = app_clone.try_state::() { - let auto_hide_paused = state.auto_hide_paused.lock().unwrap(); - if *auto_hide_paused { - return; // Auto-hide was paused during the delay - } - - let last_focus_lost = state.last_focus_lost.lock().unwrap(); - if let Some(focus_lost_time) = *last_focus_lost { - // If more than 300ms have passed since focus lost and focus wasn't regained, hide - focus_lost_time.elapsed().as_millis() >= 300 - } else { - false // Focus was regained - } - } else { - true - }; - - if should_still_hide { - if let Ok(is_focused) = window_clone.is_focused() { - if !is_focused { - let _ = window_clone.hide(); - } - } - } - }); - } - } - - // On other platforms, use the original logic - #[cfg(not(target_os = "linux"))] - { - let should_hide = if let Some(state) = app.try_state::() { - let last_shown = state.last_shown.lock().unwrap(); - if let Some(instant) = *last_shown { - instant.elapsed().as_millis() > 500 - } else { - true - } - } else { - true - }; - - if should_hide { - let _ = window.hide(); - } - } -} - -pub fn handle_window_show(app: &tauri::AppHandle) { - if let Some(state) = app.try_state::() { - let mut last_shown = state.last_shown.lock().unwrap(); - *last_shown = Some(Instant::now()); - } -} - // ============================================================================ -// Focus Management Commands (Linux dropdown fix) +// macOS Window Configuration // ============================================================================ -#[tauri::command] -pub fn pause_auto_hide(app: tauri::AppHandle) -> Result<(), String> { - if let Some(state) = app.try_state::() { - let mut auto_hide_paused = state.auto_hide_paused.lock().unwrap(); - *auto_hide_paused = true; - println!("[WINDOW] Auto-hide paused"); +/// Configure the window for macOS tray-app behavior: +/// - Visible on all Spaces (never swept away by swipe gestures) +/// - Does not auto-hide when the app loses focus +#[cfg(target_os = "macos")] +pub fn set_macos_window_level(window: &tauri::WebviewWindow) { + use objc2_app_kit::NSWindow; + + // Visible on all Spaces via Tauri native API + // (sets NSWindowCollectionBehaviorCanJoinAllSpaces under the hood) + let _ = window.set_visible_on_all_workspaces(true); + + // setHidesOnDeactivate is not exposed by Tauri — call via objc2-app-kit. + // Prevents macOS from auto-hiding the window when the app loses focus. + unsafe { + let ns_window: &NSWindow = &*window + .ns_window() + .expect("Failed to get NSWindow handle") + .cast(); + ns_window.setHidesOnDeactivate(false); } - Ok(()) } -#[tauri::command] -pub fn resume_auto_hide(app: tauri::AppHandle) -> Result<(), String> { - if let Some(state) = app.try_state::() { - let mut auto_hide_paused = state.auto_hide_paused.lock().unwrap(); - *auto_hide_paused = false; - println!("[WINDOW] Auto-hide resumed"); - } - Ok(()) -} \ No newline at end of file +#[cfg(not(target_os = "macos"))] +pub fn set_macos_window_level(_window: &tauri::WebviewWindow) { + // No-op on non-macOS platforms +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c99ae57..92f9b48 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -24,7 +24,8 @@ "visible": false, "skipTaskbar": true, "center": false, - "devtools": true + "devtools": true, + "visibleOnAllWorkspaces": true } ], "security": {