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": {