diff --git a/komorebi/src/monitor_reconciliator/mod.rs b/komorebi/src/monitor_reconciliator/mod.rs index ec8677bd..449a8eda 100644 --- a/komorebi/src/monitor_reconciliator/mod.rs +++ b/komorebi/src/monitor_reconciliator/mod.rs @@ -25,6 +25,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::OnceLock; use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicI64; use std::sync::atomic::Ordering; pub mod hidden; @@ -44,6 +45,10 @@ pub enum MonitorNotification { static ACTIVE: AtomicBool = AtomicBool::new(true); +/// Timestamp (epoch millis) of the last DisplayConnectionChange notification. +/// Used to suppress OS-initiated window minimizes during transient display events. +static LAST_DISPLAY_CHANGE_TIMESTAMP: AtomicI64 = AtomicI64::new(0); + static CHANNEL: OnceLock<(Sender, Receiver)> = OnceLock::new(); @@ -62,11 +67,40 @@ fn event_rx() -> Receiver { } pub fn send_notification(notification: MonitorNotification) { + if matches!( + notification, + MonitorNotification::DisplayConnectionChange + | MonitorNotification::ResumingFromSuspendedState + | MonitorNotification::SessionUnlocked + ) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + LAST_DISPLAY_CHANGE_TIMESTAMP.store(now, Ordering::SeqCst); + } + if event_tx().try_send(notification).is_err() { tracing::warn!("channel is full; dropping notification") } } +/// Returns true if a display connection change event was received within the +/// last `grace_period` duration. This is used by the event processor to avoid +/// treating OS-initiated minimizes (caused by transient monitor disconnects) +/// as user-initiated minimizes. +pub fn display_change_in_progress(grace_period: std::time::Duration) -> bool { + let last = LAST_DISPLAY_CHANGE_TIMESTAMP.load(Ordering::SeqCst); + if last == 0 { + return false; + } + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + (now - last) < grace_period.as_millis() as i64 +} + pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) { let dip = DISPLAY_INDEX_PREFERENCES.read(); let mut dip_ids = dip.values(); diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 9fd7e434..49bf5676 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -266,18 +266,33 @@ impl WindowManager { } } WindowManagerEvent::Minimize(_, window) => { - let mut hide = false; + // During transient display connection changes (e.g. monitor + // briefly disconnecting and reconnecting), Windows may fire + // SystemMinimizeStart for windows on the affected monitor. + // We must not treat these OS-initiated minimizes as user + // actions, otherwise the window gets removed from the + // workspace and the reconciliator cannot restore it. + if crate::monitor_reconciliator::display_change_in_progress( + std::time::Duration::from_secs(10), + ) { + tracing::debug!( + "ignoring minimize during display connection change for hwnd: {}", + window.hwnd + ); + } else { + let mut hide = false; - { - let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock(); - if !programmatically_hidden_hwnds.contains(&window.hwnd) { - hide = true; + { + let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock(); + if !programmatically_hidden_hwnds.contains(&window.hwnd) { + hide = true; + } } - } - if hide { - self.focused_workspace_mut()?.remove_window(window.hwnd)?; - self.update_focused_workspace(false, false)?; + if hide { + self.focused_workspace_mut()?.remove_window(window.hwnd)?; + self.update_focused_workspace(false, false)?; + } } } WindowManagerEvent::Hide(_, window) => {