From 7d584a809ace689d2e736093a28756d2c563b11e Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 6 Nov 2026 14:02:24 -0800 Subject: [PATCH] fix(macos): refresh the known windows after a workspace rule moved one enforce_workspace_rules moves a window between workspaces at the start of an event but known_window_ids is only rebuilt at the end of process_event, so it still said the window was on the focused workspace. A Show for the application in the same event then passed the 'already associated with another workspace' check, found the window in no container of the focused workspace, and created a container for it there again: the window ended up in the containers of both workspaces and in view on the one the rule sent it from. Windows guards the same place by looking for the window in every workspace before adding it; here the index is brought up to date when the enforcement moved something. --- crates/komorebi-macos/src/window_manager.rs | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/komorebi-macos/src/window_manager.rs b/crates/komorebi-macos/src/window_manager.rs index 62912a0d..efe7dba8 100644 --- a/crates/komorebi-macos/src/window_manager.rs +++ b/crates/komorebi-macos/src/window_manager.rs @@ -3211,6 +3211,15 @@ impl WindowManager { self.update_focused_workspace(false, false)?; } + // The windows that moved are on their new workspaces now, and `known_window_ids` is what + // a `Show` event is checked against to tell a window komorebi has put away from a new + // one. Left as it was before the move it says the window is still on the focused + // workspace, so the next `Show` for the application brings it back there as well, and the + // window is on two workspaces and in view on the one it was sent from. + if !to_move.is_empty() { + self.update_known_window_ids(); + } + Ok(()) } @@ -6025,6 +6034,28 @@ mod tests { assert_eq!(op.floating, floating); // false } + #[test] + fn known_window_ids_follow_a_window_that_a_rule_moved() { + // what `enforce_workspace_rules` does to a window, short of reading it from the + // application: out of the container on the focused workspace, into one on another + let (mut wm, _context) = tiled(DefaultLayout::BSP, &[7]); + wm.update_known_window_ids(); + assert_eq!(wm.known_window_ids.get(&7), Some(&(0, 0))); + + let monitor = wm.monitors_mut().front_mut().unwrap(); + monitor.ensure_workspace_count(2); + let window = monitor.workspaces_mut()[0].remove_window(7).unwrap(); + monitor.workspaces_mut()[1] + .new_container_for_window(window) + .unwrap(); + + // nothing has told the window manager yet, so a `Show` would still be read against 0 + assert_eq!(wm.known_window_ids.get(&7), Some(&(0, 0))); + + wm.update_known_window_ids(); + assert_eq!(wm.known_window_ids.get(&7), Some(&(0, 1))); + } + // Resizing a tile by dragging an edge. A Rect is `left, top, width, height` (`right` and // `bottom` are the size), and the work area of the monitor is 1920x1050, which makes the // right-hand stack of an UltrawideVerticalStack layout narrow enough to expose the old guard.