gardesk/gar / 83272c3

Browse files

Fix mouse clicks not working on floating windows

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
83272c3ad7cdbbf2a7cad5b95d5e363e7c6f7686
Parents
6051a9e
Tree
ae0b835

3 changed files

StatusFile+-
M gar/src/core/mod.rs 2 5
M gar/src/ipc/i3_server.rs 29 7
M gar/src/x11/events.rs 100 65
gar/src/core/mod.rsmodified
@@ -508,11 +508,8 @@ impl WindowManager {
508508
         self.current_workspace_mut().focused = Some(window);
509509
 
510510
         // Ungrab buttons on new focused window (allow clicks through)
511
-        // But keep grabs on floating windows so we can detect edge resize clicks
512
-        let is_floating = self.windows.get(&window).map_or(false, |w| w.floating);
513
-        if !is_floating {
514
-            let _ = self.conn.ungrab_button(window);
515
-        }
511
+        // Edge resize detection uses POINTER_MOTION events, not button grabs
512
+        let _ = self.conn.ungrab_button(window);
516513
 
517514
         // Update focus history - move window to front
518515
         self.focus_history.retain(|&w| w != window);
gar/src/ipc/i3_server.rsmodified
@@ -179,22 +179,39 @@ impl I3IpcServer {
179179
 
180180
     /// Process incoming requests from all clients.
181181
     /// Returns a list of (client_index, message) pairs.
182
-    /// Also cleans up stale/disconnected clients.
182
+    /// Note: Stale/disconnected clients are cleaned up AFTER requests are processed
183
+    /// to avoid index invalidation issues.
183184
     pub fn poll_requests(&mut self) -> Vec<(usize, I3Message)> {
184185
         let mut requests = Vec::new();
185
-        let mut to_remove = Vec::new();
186186
 
187187
         for (i, client) in self.clients.iter_mut().enumerate() {
188188
             match client.read_message() {
189189
                 ReadResult::Message(msg) => requests.push((i, msg)),
190
+                ReadResult::Disconnected | ReadResult::WouldBlock => {}
191
+            }
192
+        }
193
+
194
+        requests
195
+    }
196
+
197
+    /// Clean up disconnected and stale clients.
198
+    /// Call this AFTER processing requests to avoid index invalidation.
199
+    pub fn cleanup_clients(&mut self) {
200
+        let mut to_remove = Vec::new();
201
+
202
+        for (i, client) in self.clients.iter_mut().enumerate() {
203
+            // Try a non-destructive check - peek for disconnection
204
+            match client.read_message() {
190205
                 ReadResult::Disconnected => to_remove.push(i),
191206
                 ReadResult::WouldBlock => {
192
-                    // Check if this client is stale (never sent anything, no subscriptions)
193207
                     if client.is_stale() {
194208
                         tracing::debug!("Cleaning up stale i3 IPC client (no activity for {:?})", IDLE_CLIENT_TIMEOUT);
195209
                         to_remove.push(i);
196210
                     }
197211
                 }
212
+                ReadResult::Message(_) => {
213
+                    // Message will be processed next poll cycle
214
+                }
198215
             }
199216
         }
200217
 
@@ -202,8 +219,6 @@ impl I3IpcServer {
202219
         for i in to_remove.into_iter().rev() {
203220
             self.clients.remove(i);
204221
         }
205
-
206
-        requests
207222
     }
208223
 
209224
     /// Send a response to a specific client.
@@ -225,17 +240,24 @@ impl I3IpcServer {
225240
     /// Broadcast a workspace event to all subscribed clients.
226241
     pub fn broadcast_workspace_event(&mut self, json: &str) {
227242
         let mut disconnected = Vec::new();
243
+        let subscribed_count = self.clients.iter().filter(|c| c.is_subscribed("workspace")).count();
244
+        tracing::debug!("Broadcasting workspace event to {} subscribed clients (of {} total)", subscribed_count, self.clients.len());
228245
 
229246
         for (i, client) in self.clients.iter_mut().enumerate() {
230247
             if client.is_subscribed("workspace") {
231
-                if client.send_event(EventType::Workspace, json).is_err() {
232
-                    disconnected.push(i);
248
+                match client.send_event(EventType::Workspace, json) {
249
+                    Ok(_) => tracing::debug!("Sent workspace event to client {}", i),
250
+                    Err(e) => {
251
+                        tracing::warn!("Failed to send workspace event to client {}: {}", i, e);
252
+                        disconnected.push(i);
253
+                    }
233254
                 }
234255
             }
235256
         }
236257
 
237258
         // Remove failed clients
238259
         for i in disconnected.into_iter().rev() {
260
+            tracing::debug!("Removing disconnected client {}", i);
239261
             self.clients.remove(i);
240262
         }
241263
     }
gar/src/x11/events.rsmodified
@@ -233,10 +233,6 @@ impl WindowManager {
233233
             // Focus the first window
234234
             if let Some(window) = self.focused_window {
235235
                 self.set_focus(window, true)?;
236
-                // Ungrab button for click-through (unless floating - keep for edge resize)
237
-                if !self.is_floating(window) {
238
-                    self.conn.ungrab_button(window)?;
239
-                }
240236
             }
241237
             tracing::info!("Adopted {} existing windows", adopted);
242238
         }
@@ -774,21 +770,10 @@ impl WindowManager {
774770
 
775771
         tracing::debug!("Focus follows mouse: focusing window {}", window);
776772
 
777
-        // Regrab button on old focused window (unless floating - keep for edge resize)
778
-        if let Some(old) = self.focused_window {
779
-            if !self.is_floating(old) {
780
-                self.conn.grab_button(old)?;
781
-            }
782
-        }
783
-
784773
         // Focus the new window (no warp - mouse enter)
774
+        // set_focus handles grab/ungrab for old and new windows
785775
         self.set_focus(window, false)?;
786776
 
787
-        // Ungrab button for click-through (unless floating - keep for edge resize)
788
-        if !self.is_floating(window) {
789
-            self.conn.ungrab_button(window)?;
790
-        }
791
-
792777
         // Raise floating windows on focus
793778
         if self.is_floating(window) {
794779
             self.raise_window(window)?;
@@ -1130,12 +1115,9 @@ impl WindowManager {
11301115
         let geometries = self.current_workspace().tree.calculate_geometries(screen);
11311116
 
11321117
         if let Some(target) = Node::find_adjacent(&geometries, focused, direction) {
1133
-            // Regrab button on old window
1134
-            self.conn.grab_button(focused)?;
1135
-
11361118
             // Focus new window (keyboard navigation, warp pointer)
1119
+            // set_focus handles grab/ungrab for old and new windows
11371120
             self.set_focus(target, true)?;
1138
-            self.conn.ungrab_button(target)?;
11391121
             self.conn.flush()?;
11401122
 
11411123
             tracing::debug!("Focused {:?} to window {}", direction, target);
@@ -1185,17 +1167,8 @@ impl WindowManager {
11851167
             .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
11861168
             .or_else(|| self.workspaces[workspace_idx].tree.first_window())
11871169
         {
1188
-            // Regrab on old window (unless floating)
1189
-            if let Some(old) = self.focused_window {
1190
-                if !self.is_floating(old) {
1191
-                    self.conn.grab_button(old)?;
1192
-                }
1193
-            }
1170
+            // set_focus handles grab/ungrab for old and new windows
11941171
             self.set_focus(window, true)?;
1195
-            // Ungrab on new window (unless floating - keep for edge resize)
1196
-            if !self.is_floating(window) {
1197
-                self.conn.ungrab_button(window)?;
1198
-            }
11991172
         } else {
12001173
             // No windows on target monitor - clear focus and warp to monitor center
12011174
             self.focused_window = None;
@@ -1271,10 +1244,65 @@ impl WindowManager {
12711244
         Ok(())
12721245
     }
12731246
 
1247
+    /// Execute an i3-compatible command (from IPC RUN_COMMAND).
1248
+    /// Returns true if the command was executed successfully.
1249
+    fn execute_i3_command(&mut self, cmd: &str) -> bool {
1250
+        let cmd = cmd.trim();
1251
+        tracing::debug!("Executing i3 command: {}", cmd);
1252
+
1253
+        // Parse "workspace <name|number>" command
1254
+        // Use switch_workspace_impl with warp_pointer=false since IPC commands
1255
+        // (like clicks from the bar) shouldn't move the mouse cursor
1256
+        if let Some(rest) = cmd.strip_prefix("workspace ") {
1257
+            let rest = rest.trim();
1258
+            // Try to parse as number first
1259
+            if let Ok(num) = rest.parse::<usize>() {
1260
+                // Workspace numbers are 1-indexed in i3
1261
+                let idx = num.saturating_sub(1);
1262
+                if idx < self.workspaces.len() {
1263
+                    if let Err(e) = self.switch_workspace_impl(idx, false) {
1264
+                        tracing::warn!("Failed to switch workspace: {}", e);
1265
+                        return false;
1266
+                    }
1267
+                    return true;
1268
+                }
1269
+            }
1270
+            // Try to match by name
1271
+            if let Some(idx) = self.workspaces.iter().position(|ws| ws.name == rest) {
1272
+                if let Err(e) = self.switch_workspace_impl(idx, false) {
1273
+                    tracing::warn!("Failed to switch workspace: {}", e);
1274
+                    return false;
1275
+                }
1276
+                return true;
1277
+            }
1278
+            tracing::warn!("Workspace not found: {}", rest);
1279
+            return false;
1280
+        }
1281
+
1282
+        // Parse "workspace number <n>" command
1283
+        if let Some(rest) = cmd.strip_prefix("workspace number ") {
1284
+            if let Ok(num) = rest.trim().parse::<usize>() {
1285
+                let idx = num.saturating_sub(1);
1286
+                if idx < self.workspaces.len() {
1287
+                    if let Err(e) = self.switch_workspace_impl(idx, false) {
1288
+                        tracing::warn!("Failed to switch workspace: {}", e);
1289
+                        return false;
1290
+                    }
1291
+                    return true;
1292
+                }
1293
+            }
1294
+            return false;
1295
+        }
1296
+
1297
+        tracing::debug!("Unknown i3 command: {}", cmd);
1298
+        false
1299
+    }
1300
+
12741301
     /// Switch to workspace using i3-style behavior:
12751302
     /// - If workspace is visible on another monitor, focus moves to that monitor
12761303
     /// - If workspace is not visible, it appears on the current monitor
1277
-    fn switch_workspace(&mut self, idx: usize) -> Result<()> {
1304
+    /// If warp_pointer is false, the mouse cursor is not moved (for IPC commands).
1305
+    fn switch_workspace_impl(&mut self, idx: usize, warp_pointer: bool) -> Result<()> {
12781306
         if idx >= self.workspaces.len() {
12791307
             return Ok(());
12801308
         }
@@ -1304,19 +1332,21 @@ impl WindowManager {
13041332
             // Update EWMH
13051333
             self.conn.set_current_desktop(idx as u32)?;
13061334
 
1307
-            // Warp pointer to that monitor
1308
-            let monitor_geom = self.monitors[monitor_idx].geometry;
1309
-            let center_x = monitor_geom.x + (monitor_geom.width as i16 / 2);
1310
-            let center_y = monitor_geom.y + (monitor_geom.height as i16 / 2);
1311
-            self.conn.warp_pointer(center_x, center_y)?;
1312
-            self.last_warp = std::time::Instant::now();
1335
+            // Warp pointer to that monitor (only if requested)
1336
+            if warp_pointer {
1337
+                let monitor_geom = self.monitors[monitor_idx].geometry;
1338
+                let center_x = monitor_geom.x + (monitor_geom.width as i16 / 2);
1339
+                let center_y = monitor_geom.y + (monitor_geom.height as i16 / 2);
1340
+                self.conn.warp_pointer(center_x, center_y)?;
1341
+                self.last_warp = std::time::Instant::now();
1342
+            }
13131343
 
13141344
             // Focus a window on that workspace
13151345
             if let Some(window) = self.workspaces[idx].focused
13161346
                 .or_else(|| self.workspaces[idx].floating.last().copied())
13171347
                 .or_else(|| self.workspaces[idx].tree.first_window())
13181348
             {
1319
-                self.set_focus(window, true)?;
1349
+                self.set_focus(window, warp_pointer)?;
13201350
             } else {
13211351
                 self.focused_window = None;
13221352
                 self.conn.set_active_window(None)?;
@@ -1367,16 +1397,18 @@ impl WindowManager {
13671397
                 .or_else(|| self.workspaces[idx].floating.last().copied())
13681398
                 .or_else(|| self.workspaces[idx].tree.first_window())
13691399
             {
1370
-                self.set_focus(window, true)?;
1400
+                self.set_focus(window, warp_pointer)?;
13711401
             } else {
1372
-                // No windows - warp to center of monitor
1402
+                // No windows - warp to center of monitor (only if requested)
13731403
                 self.focused_window = None;
13741404
                 self.conn.set_active_window(None)?;
1375
-                let monitor_geom = self.monitors[current_monitor].geometry;
1376
-                let center_x = monitor_geom.x + (monitor_geom.width as i16 / 2);
1377
-                let center_y = monitor_geom.y + (monitor_geom.height as i16 / 2);
1378
-                self.conn.warp_pointer(center_x, center_y)?;
1379
-                self.last_warp = std::time::Instant::now();
1405
+                if warp_pointer {
1406
+                    let monitor_geom = self.monitors[current_monitor].geometry;
1407
+                    let center_x = monitor_geom.x + (monitor_geom.width as i16 / 2);
1408
+                    let center_y = monitor_geom.y + (monitor_geom.height as i16 / 2);
1409
+                    self.conn.warp_pointer(center_x, center_y)?;
1410
+                    self.last_warp = std::time::Instant::now();
1411
+                }
13801412
             }
13811413
         }
13821414
 
@@ -1389,6 +1421,11 @@ impl WindowManager {
13891421
         Ok(())
13901422
     }
13911423
 
1424
+    /// Switch to workspace with pointer warping (default behavior for keybinds).
1425
+    fn switch_workspace(&mut self, idx: usize) -> Result<()> {
1426
+        self.switch_workspace_impl(idx, true)
1427
+    }
1428
+
13921429
     fn move_to_workspace(&mut self, idx: usize) -> Result<()> {
13931430
         if idx >= self.workspaces.len() {
13941431
             return Ok(());
@@ -1900,17 +1937,8 @@ impl WindowManager {
19001937
             .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
19011938
             .or_else(|| self.workspaces[workspace_idx].tree.first_window())
19021939
         {
1903
-            // Regrab on old window (unless floating)
1904
-            if let Some(old) = self.focused_window {
1905
-                if !self.is_floating(old) {
1906
-                    self.conn.grab_button(old)?;
1907
-                }
1908
-            }
1940
+            // set_focus handles grab/ungrab for old and new windows
19091941
             self.set_focus(window, true)?;
1910
-            // Ungrab on new window (unless floating - keep for edge resize)
1911
-            if !self.is_floating(window) {
1912
-                self.conn.ungrab_button(window)?;
1913
-            }
19141942
         } else {
19151943
             // No windows - warp to monitor center
19161944
             self.focused_window = None;
@@ -2021,15 +2049,8 @@ impl WindowManager {
20212049
 
20222050
         let next_window = floating[next_idx];
20232051
 
2024
-        // Regrab button on old focused window (unless it's floating)
2025
-        if let Some(old) = self.focused_window {
2026
-            if !self.is_floating(old) {
2027
-                self.conn.grab_button(old)?;
2028
-            }
2029
-        }
2030
-
20312052
         // Focus and raise the next floating window (keyboard action, warp pointer)
2032
-        // Keep button grabbed on floating windows for edge resize
2053
+        // set_focus handles grab/ungrab for old and new windows
20332054
         self.set_focus(next_window, true)?;
20342055
         self.raise_window(next_window)?;
20352056
 
@@ -2171,6 +2192,8 @@ impl WindowManager {
21712192
             match MessageType::from_u32(msg_type) {
21722193
                 Some(MessageType::GetWorkspaces) => {
21732194
                     let workspaces = self.build_i3_workspaces();
2195
+                    let focused_ws: Vec<_> = workspaces.iter().filter(|w| w.focused).map(|w| &w.name).collect();
2196
+                    tracing::debug!("GET_WORKSPACES: returning {} workspaces, focused: {:?}", workspaces.len(), focused_ws);
21742197
                     let json = build_workspaces_json(&workspaces);
21752198
                     if let Some(ref mut i3_ipc) = self.i3_ipc_server {
21762199
                         i3_ipc.send_response(client_idx, msg_type, &json);
@@ -2204,8 +2227,14 @@ impl WindowManager {
22042227
                     }
22052228
                 }
22062229
                 Some(MessageType::RunCommand) => {
2207
-                    // Return success for now - we could parse and execute i3 commands later
2208
-                    let json = r#"[{"success":true}]"#;
2230
+                    // Parse and execute i3-compatible commands
2231
+                    let cmd_str = msg.payload_str().unwrap_or("");
2232
+                    let success = self.execute_i3_command(cmd_str);
2233
+                    let json = if success {
2234
+                        r#"[{"success":true}]"#
2235
+                    } else {
2236
+                        r#"[{"success":false,"error":"command failed"}]"#
2237
+                    };
22092238
                     if let Some(ref mut i3_ipc) = self.i3_ipc_server {
22102239
                         i3_ipc.send_response(client_idx, msg_type, json);
22112240
                     }
@@ -2220,6 +2249,12 @@ impl WindowManager {
22202249
             }
22212250
         }
22222251
 
2252
+        // Clean up disconnected/stale clients AFTER processing requests
2253
+        // to avoid index invalidation during send_response
2254
+        if let Some(ref mut i3_ipc) = self.i3_ipc_server {
2255
+            i3_ipc.cleanup_clients();
2256
+        }
2257
+
22232258
         Ok(())
22242259
     }
22252260