gardesk/tarmac / 13d8bb3

Browse files

Bias external focus resolver toward visible workspaces

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
13d8bb31e9f18e645f9df58fb14fc2b2f958c9a3
Parents
fdbe276
Tree
3dd1cd9

1 changed file

StatusFile+-
M tarmac/src/core/state.rs 91 18
tarmac/src/core/state.rsmodified
@@ -970,27 +970,39 @@ impl WmState {
970970
             return Some(id);
971971
         }
972972
 
973
-        for ws in self.workspaces.iter() {
974
-            for &wid in ws.focus_history.iter().rev() {
975
-                if self
976
-                    .registry
977
-                    .get(wid)
978
-                    .is_some_and(|window| window.app_pid == pid)
979
-                    && self.is_external_focus_candidate(wid)
980
-                {
981
-                    return Some(wid);
982
-                }
973
+        // Prefer windows on currently visible workspaces over hidden ones, and
974
+        // the focused monitor's workspace over other visible ones. Without this
975
+        // bias the resolver iterates by workspace index, so workspace 1 (index
976
+        // 0) acts as a magnet for any cross-workspace external focus event and
977
+        // can yank the WM back across workspaces it just left.
978
+        let active_ws = self
979
+            .monitors
980
+            .get(self.focused_monitor)
981
+            .map(|m| m.active_workspace);
982
+
983
+        if let Some(ws_idx) = active_ws
984
+            && let Some(wid) = self.find_pid_target_in_workspace(ws_idx, pid)
985
+        {
986
+            return Some(wid);
987
+        }
988
+
989
+        for (ws_idx, _) in self.workspaces.iter().enumerate() {
990
+            if Some(ws_idx) == active_ws {
991
+                continue;
992
+            }
993
+            if self.monitor_showing_workspace(ws_idx).is_none() {
994
+                continue;
995
+            }
996
+            if let Some(wid) = self.find_pid_target_in_workspace(ws_idx, pid) {
997
+                return Some(wid);
983998
             }
984999
         }
9851000
 
986
-        for ws in self.workspaces.iter() {
987
-            if let Some(wid) = ws.focused
988
-                && self
989
-                    .registry
990
-                    .get(wid)
991
-                    .is_some_and(|window| window.app_pid == pid)
992
-                && self.is_external_focus_candidate(wid)
993
-            {
1001
+        for (ws_idx, _) in self.workspaces.iter().enumerate() {
1002
+            if self.monitor_showing_workspace(ws_idx).is_some() {
1003
+                continue;
1004
+            }
1005
+            if let Some(wid) = self.find_pid_target_in_workspace(ws_idx, pid) {
9941006
                 return Some(wid);
9951007
             }
9961008
         }
@@ -1002,6 +1014,21 @@ impl WmState {
10021014
             .find(|id| self.is_external_focus_candidate(*id))
10031015
     }
10041016
 
1017
+    fn find_pid_target_in_workspace(&self, ws_idx: usize, pid: i32) -> Option<WindowId> {
1018
+        let ws = self.workspaces.get(ws_idx);
1019
+        for &wid in ws.focus_history.iter().rev() {
1020
+            if self
1021
+                .registry
1022
+                .get(wid)
1023
+                .is_some_and(|window| window.app_pid == pid)
1024
+                && self.is_external_focus_candidate(wid)
1025
+            {
1026
+                return Some(wid);
1027
+            }
1028
+        }
1029
+        None
1030
+    }
1031
+
10051032
     fn adopt_external_focus(&mut self, id: WindowId) {
10061033
         let Some(ws_idx) = self.workspaces.find_window(id) else {
10071034
             return;
@@ -3695,6 +3722,52 @@ mod tests {
36953722
         assert!(state.ffm_cooldown_until.is_none());
36963723
     }
36973724
 
3725
+    #[test]
3726
+    fn external_app_focus_prefers_visible_workspace_over_workspace_one() {
3727
+        // Regression: a stale focus_history entry for `pid` on workspace 1
3728
+        // (index 0) used to win over a candidate on the currently visible
3729
+        // workspace, yanking the WM back to ws1 in a feedback loop with the
3730
+        // 50ms frontmost-app poll.
3731
+        let mut state = WmState::new();
3732
+        state.monitors = vec![Monitor {
3733
+            id: 42,
3734
+            frame: Rect::new(0.0, 0.0, 1920.0, 1080.0),
3735
+            usable_frame: Rect::new(0.0, 33.0, 1920.0, 1047.0),
3736
+            is_primary: true,
3737
+            active_workspace: 0,
3738
+        }];
3739
+        let screen = state.monitors[0].usable_frame;
3740
+
3741
+        state.registry.add(tracked_window(70, 13, "WezTerm"));
3742
+        state.registry.add(tracked_window(71, 13, "WezTerm"));
3743
+        state
3744
+            .workspaces
3745
+            .get_or_create_target(&WorkspaceTarget::Numbered(2));
3746
+
3747
+        // Stale history on ws1 for pid 13.
3748
+        {
3749
+            let ws = state.workspaces.get_mut(0);
3750
+            ws.tree.insert_with_rect(70, None, screen);
3751
+            ws.record_focus(70);
3752
+        }
3753
+        // Currently visible ws2 also has a window for pid 13.
3754
+        {
3755
+            let ws = state.workspaces.get_mut(1);
3756
+            ws.tree.insert_with_rect(71, None, screen);
3757
+            ws.record_focus(71);
3758
+        }
3759
+
3760
+        // Make ws2 the visible workspace.
3761
+        state.monitors[0].active_workspace = 1;
3762
+        state.sync_workspace_visibility();
3763
+
3764
+        state.adopt_external_app_focus(13, None);
3765
+
3766
+        // Resolver must prefer the ws2 window over the stale ws1 entry.
3767
+        assert_eq!(state.monitors[0].active_workspace, 1);
3768
+        assert_eq!(state.active_workspace().focused, Some(71));
3769
+    }
3770
+
36983771
     #[test]
36993772
     fn external_app_focus_dismisses_overlay_before_switching() {
37003773
         let mut state = WmState::new();