From bf432bd51a893196f82efcedf2cbe14765418e79 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas <51421+MattKotsenas@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:38:56 -0700 Subject: [PATCH] fix(wm): refocus selection on destroy when foreground goes null Reactive events pass trigger_focus=false so komorebi follows the OS foreground and reconciles via the FocusChange handler once Windows promotes a successor window. When the destroyed window was the foreground and Windows promotes nothing, the foreground becomes null, no FocusChange arrives, and komorebi's selection never regains OS focus, leaving every border unfocused until the user refocuses by hand. Add a narrow fallback in the Destroy/Unmanage handler: when the OS foreground is null after removal, focus the current selection. Any non-null foreground is left to FocusChange, so the OS is not fought and the should_skip_focus_change desync cannot occur. --- komorebi/src/process_event.rs | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 315180c98..8c3797bd0 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -45,6 +45,16 @@ fn should_skip_focus_change(foreground_hwnd: Option, window_hwnd: isize) matches!(foreground_hwnd, Some(hwnd) if hwnd != window_hwnd) } +/// After a managed window is destroyed, Windows may promote no successor, leaving +/// the foreground null with no FocusChange to reconcile from. The selection then +/// never regains focus: every border renders unfocused and keystrokes fall through +/// to nothing until the user refocuses by hand. Refocus only in that case, otherwise +/// foreground successor is left to FocusChange so we don't fight the OS or desync +/// focus. +fn should_refocus_after_destroy(foreground_hwnd: Option) -> bool { + matches!(foreground_hwnd, None | Some(0)) +} + #[cfg(test)] mod focus_change_tests { use super::should_skip_focus_change; @@ -65,6 +75,26 @@ mod focus_change_tests { } } +#[cfg(test)] +mod destroy_focus_tests { + use super::should_refocus_after_destroy; + + #[test] + fn refocuses_when_foreground_is_null() { + assert!(should_refocus_after_destroy(Some(0))); + } + + #[test] + fn refocuses_when_foreground_is_unavailable() { + assert!(should_refocus_after_destroy(None)); + } + + #[test] + fn defers_when_a_successor_took_the_foreground() { + assert!(!should_refocus_after_destroy(Some(12345))); + } +} + #[tracing::instrument] pub fn listen_for_events(wm: Arc>) { let receiver = wm.lock().incoming_events.clone(); @@ -282,7 +312,13 @@ impl WindowManager { WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => { if self.focused_workspace()?.contains_window(window.hwnd) { self.focused_workspace_mut()?.remove_window(window.hwnd)?; - self.update_focused_workspace(false, false)?; + + // With refocus true, update_focused_workspace focuses the surviving + // selection (maximized/monocle/tiling); with refocus false it only + // re-tiles. + let refocus = + should_refocus_after_destroy(WindowsApi::foreground_window().ok()); + self.update_focused_workspace(refocus, refocus)?; let mut already_moved_window_handles = self.already_moved_window_handles.lock();