Skip to content

Commit 73af11a

Browse files
committed
fix: windows management for macOS (#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. Closes #17 Assisted-by: Claude Sonnet 4.6 (OpenCode, LLMProxy) Signed-off-by: Pierre-Yves Lapersonne <pierreyves.lapersonne@orange.com>
1 parent 5982b1d commit 73af11a

7 files changed

Lines changed: 111 additions & 162 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"scripts": {
55
"ng": "ng",
66
"start": "npm run tauri:serve",
7-
"web:serve": "ng serve -o",
7+
"web:serve": "ng serve",
88
"web:build": "ng build --base-href ./",
99
"web:dev": "npm run web:build",
1010
"web:prod": "npm run web:build -- -c production",

src-tauri/Cargo.lock

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ tauri-plugin-single-instance = "^2.0"
3333
tauri-plugin-autostart = "^2.0"
3434
urlencoding = "^2.1"
3535

36+
[target.'cfg(target_os = "macos")'.dependencies]
37+
objc2-app-kit = "0.3.2"
38+
3639
[features]
3740
# by default Tauri runs in production mode
3841
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL

src-tauri/src/main.rs

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mod updater;
3737
use config::load_config;
3838
use state::AppState;
3939
use tray::generate_tray_icon;
40-
use window::{position_window_near_tray, handle_window_focus_lost, handle_window_show};
40+
use window::{position_window_near_tray, set_macos_window_level};
4141

4242
fn main() {
4343
// Load environment variables from .env file
@@ -65,12 +65,14 @@ fn main() {
6565
))
6666
.manage(AppState {
6767
alert_count: Mutex::new(0),
68-
last_shown: Mutex::new(None),
69-
last_focus_lost: Mutex::new(None),
70-
auto_hide_paused: Mutex::new(false),
7168
config: Mutex::new(config),
7269
})
7370
.setup(|app| {
71+
// On macOS, use Accessory policy so the window floats over all Spaces
72+
// and is not bound to a single Space like a regular app window.
73+
#[cfg(target_os = "macos")]
74+
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
75+
7476
// Enable autostart on first run
7577
use tauri_plugin_autostart::ManagerExt;
7678
let autostart_manager = app.autolaunch();
@@ -96,26 +98,13 @@ fn main() {
9698
let icon_data = generate_tray_icon(None, has_repos);
9799
let icon = Image::from_bytes(&icon_data)?;
98100

99-
// Check if user is authenticated
100-
let is_authenticated = {
101-
let state = app.state::<AppState>();
102-
let config = state.config.lock().unwrap();
103-
config.access_token.as_ref()
104-
.map(|t| !t.trim().is_empty())
105-
.unwrap_or(false)
106-
};
107-
108-
// Show window if not authenticated, hide if already logged in
101+
// Always show the window on startup — Angular handles the correct
102+
// view (login vs alerts) based on auth status via checkAuthStatus()
109103
if let Some(window) = app.get_webview_window("main") {
110-
if is_authenticated {
111-
let _ = window.hide();
112-
} else {
113-
// First time - show window for login
114-
handle_window_show(app.handle());
115-
position_window_near_tray(&window);
116-
let _ = window.show();
117-
let _ = window.set_focus();
118-
}
104+
set_macos_window_level(&window);
105+
position_window_near_tray(&window);
106+
let _ = window.show();
107+
let _ = window.set_focus();
119108
}
120109

121110

@@ -165,7 +154,7 @@ fn main() {
165154
if window.is_visible().unwrap_or(false) {
166155
let _ = window.hide();
167156
} else {
168-
handle_window_show(&app);
157+
set_macos_window_level(&window);
169158
position_window_near_tray(&window);
170159
let _ = window.show();
171160
let _ = window.set_focus();
@@ -179,21 +168,11 @@ fn main() {
179168
Ok(())
180169
})
181170
.on_window_event(|window, event| {
171+
// Intercept close — hide instead of quit (tray app)
182172
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
183173
let _ = window.hide();
184174
api.prevent_close();
185175
}
186-
if let tauri::WindowEvent::Focused(focused) = event {
187-
if !*focused {
188-
handle_window_focus_lost(window);
189-
} else {
190-
// Window regained focus - clear the focus lost timestamp
191-
if let Some(state) = window.app_handle().try_state::<AppState>() {
192-
let mut last_focus_lost = state.last_focus_lost.lock().unwrap();
193-
*last_focus_lost = None;
194-
}
195-
}
196-
}
197176
})
198177
.invoke_handler(tauri::generate_handler![
199178
auth::set_token,
@@ -209,8 +188,6 @@ fn main() {
209188
alerts::get_github_security_alerts,
210189
tray::update_tray_icon,
211190
system::open_taskbar_settings,
212-
window::pause_auto_hide,
213-
window::resume_auto_hide,
214191
updater::check_for_updates,
215192
updater::install_update,
216193
updater::get_current_version,

src-tauri/src/state.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,9 @@
1010
*/
1111

1212
use std::sync::Mutex;
13-
use std::time::Instant;
1413
use crate::models::AppConfig;
1514

1615
pub struct AppState {
1716
pub alert_count: Mutex<usize>,
18-
pub last_shown: Mutex<Option<Instant>>,
19-
pub last_focus_lost: Mutex<Option<Instant>>,
20-
pub auto_hide_paused: Mutex<bool>,
2117
pub config: Mutex<AppConfig>,
2218
}

src-tauri/src/window.rs

Lines changed: 25 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
* Software description: A modern desktop application that monitors security vulnerabilities across your GitHub repositories in real-time.
1010
*/
1111

12-
use tauri::{PhysicalPosition, Manager, LogicalSize};
13-
use std::time::Instant;
14-
use crate::state::AppState;
12+
use tauri::{PhysicalPosition, LogicalSize};
1513

1614
// ============================================================================
1715
// Window Management
@@ -51,125 +49,33 @@ pub fn position_window_near_tray(window: &tauri::WebviewWindow) {
5149
}
5250
}
5351

54-
pub fn handle_window_focus_lost(window: &tauri::Window) {
55-
let app = window.app_handle();
56-
57-
// Check if auto-hide is paused (for dropdown interactions)
58-
let is_paused = if let Some(state) = app.try_state::<AppState>() {
59-
let auto_hide_paused = state.auto_hide_paused.lock().unwrap();
60-
*auto_hide_paused
61-
} else {
62-
false
63-
};
64-
65-
if is_paused {
66-
println!("[WINDOW] Auto-hide paused - ignoring focus loss");
67-
return;
68-
}
69-
70-
// On Linux, use a delayed hide approach to handle dropdown interactions
71-
#[cfg(target_os = "linux")]
72-
{
73-
let should_hide = if let Some(state) = app.try_state::<AppState>() {
74-
let last_shown = state.last_shown.lock().unwrap();
75-
if let Some(instant) = *last_shown {
76-
instant.elapsed().as_millis() > 1000 // 1 second minimum on Linux
77-
} else {
78-
true
79-
}
80-
} else {
81-
true
82-
};
83-
84-
if should_hide {
85-
let window_clone = window.clone();
86-
let app_clone = app.clone();
87-
88-
// Store the focus lost time
89-
if let Some(state) = app.try_state::<AppState>() {
90-
let mut last_focus_lost = state.last_focus_lost.lock().unwrap();
91-
*last_focus_lost = Some(Instant::now());
92-
}
93-
94-
std::thread::spawn(move || {
95-
std::thread::sleep(std::time::Duration::from_millis(300)); // Wait 300ms
96-
97-
// Check if auto-hide is still not paused and focus wasn't regained
98-
let should_still_hide = if let Some(state) = app_clone.try_state::<AppState>() {
99-
let auto_hide_paused = state.auto_hide_paused.lock().unwrap();
100-
if *auto_hide_paused {
101-
return; // Auto-hide was paused during the delay
102-
}
103-
104-
let last_focus_lost = state.last_focus_lost.lock().unwrap();
105-
if let Some(focus_lost_time) = *last_focus_lost {
106-
// If more than 300ms have passed since focus lost and focus wasn't regained, hide
107-
focus_lost_time.elapsed().as_millis() >= 300
108-
} else {
109-
false // Focus was regained
110-
}
111-
} else {
112-
true
113-
};
114-
115-
if should_still_hide {
116-
if let Ok(is_focused) = window_clone.is_focused() {
117-
if !is_focused {
118-
let _ = window_clone.hide();
119-
}
120-
}
121-
}
122-
});
123-
}
124-
}
125-
126-
// On other platforms, use the original logic
127-
#[cfg(not(target_os = "linux"))]
128-
{
129-
let should_hide = if let Some(state) = app.try_state::<AppState>() {
130-
let last_shown = state.last_shown.lock().unwrap();
131-
if let Some(instant) = *last_shown {
132-
instant.elapsed().as_millis() > 500
133-
} else {
134-
true
135-
}
136-
} else {
137-
true
138-
};
139-
140-
if should_hide {
141-
let _ = window.hide();
142-
}
143-
}
144-
}
145-
146-
pub fn handle_window_show(app: &tauri::AppHandle) {
147-
if let Some(state) = app.try_state::<AppState>() {
148-
let mut last_shown = state.last_shown.lock().unwrap();
149-
*last_shown = Some(Instant::now());
150-
}
151-
}
152-
15352
// ============================================================================
154-
// Focus Management Commands (Linux dropdown fix)
53+
// macOS Window Configuration
15554
// ============================================================================
15655

157-
#[tauri::command]
158-
pub fn pause_auto_hide(app: tauri::AppHandle) -> Result<(), String> {
159-
if let Some(state) = app.try_state::<AppState>() {
160-
let mut auto_hide_paused = state.auto_hide_paused.lock().unwrap();
161-
*auto_hide_paused = true;
162-
println!("[WINDOW] Auto-hide paused");
56+
/// Configure the window for macOS tray-app behavior:
57+
/// - Visible on all Spaces (never swept away by swipe gestures)
58+
/// - Does not auto-hide when the app loses focus
59+
#[cfg(target_os = "macos")]
60+
pub fn set_macos_window_level(window: &tauri::WebviewWindow) {
61+
use objc2_app_kit::NSWindow;
62+
63+
// Visible on all Spaces via Tauri native API
64+
// (sets NSWindowCollectionBehaviorCanJoinAllSpaces under the hood)
65+
let _ = window.set_visible_on_all_workspaces(true);
66+
67+
// setHidesOnDeactivate is not exposed by Tauri — call via objc2-app-kit.
68+
// Prevents macOS from auto-hiding the window when the app loses focus.
69+
unsafe {
70+
let ns_window: &NSWindow = &*window
71+
.ns_window()
72+
.expect("Failed to get NSWindow handle")
73+
.cast();
74+
ns_window.setHidesOnDeactivate(false);
16375
}
164-
Ok(())
16576
}
16677

167-
#[tauri::command]
168-
pub fn resume_auto_hide(app: tauri::AppHandle) -> Result<(), String> {
169-
if let Some(state) = app.try_state::<AppState>() {
170-
let mut auto_hide_paused = state.auto_hide_paused.lock().unwrap();
171-
*auto_hide_paused = false;
172-
println!("[WINDOW] Auto-hide resumed");
173-
}
174-
Ok(())
175-
}
78+
#[cfg(not(target_os = "macos"))]
79+
pub fn set_macos_window_level(_window: &tauri::WebviewWindow) {
80+
// No-op on non-macOS platforms
81+
}

src-tauri/tauri.conf.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"visible": false,
2525
"skipTaskbar": true,
2626
"center": false,
27-
"devtools": true
27+
"devtools": true,
28+
"visibleOnAllWorkspaces": true
2829
}
2930
],
3031
"security": {

0 commit comments

Comments
 (0)