@@ -233,10 +233,6 @@ impl WindowManager { |
| 233 | 233 | // Focus the first window |
| 234 | 234 | if let Some(window) = self.focused_window { |
| 235 | 235 | 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 | | - } |
| 240 | 236 | } |
| 241 | 237 | tracing::info!("Adopted {} existing windows", adopted); |
| 242 | 238 | } |
@@ -774,21 +770,10 @@ impl WindowManager { |
| 774 | 770 | |
| 775 | 771 | tracing::debug!("Focus follows mouse: focusing window {}", window); |
| 776 | 772 | |
| 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 | | - |
| 784 | 773 | // Focus the new window (no warp - mouse enter) |
| 774 | + // set_focus handles grab/ungrab for old and new windows |
| 785 | 775 | self.set_focus(window, false)?; |
| 786 | 776 | |
| 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 | | - |
| 792 | 777 | // Raise floating windows on focus |
| 793 | 778 | if self.is_floating(window) { |
| 794 | 779 | self.raise_window(window)?; |
@@ -1130,12 +1115,9 @@ impl WindowManager { |
| 1130 | 1115 | let geometries = self.current_workspace().tree.calculate_geometries(screen); |
| 1131 | 1116 | |
| 1132 | 1117 | if let Some(target) = Node::find_adjacent(&geometries, focused, direction) { |
| 1133 | | - // Regrab button on old window |
| 1134 | | - self.conn.grab_button(focused)?; |
| 1135 | | - |
| 1136 | 1118 | // Focus new window (keyboard navigation, warp pointer) |
| 1119 | + // set_focus handles grab/ungrab for old and new windows |
| 1137 | 1120 | self.set_focus(target, true)?; |
| 1138 | | - self.conn.ungrab_button(target)?; |
| 1139 | 1121 | self.conn.flush()?; |
| 1140 | 1122 | |
| 1141 | 1123 | tracing::debug!("Focused {:?} to window {}", direction, target); |
@@ -1185,17 +1167,8 @@ impl WindowManager { |
| 1185 | 1167 | .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) |
| 1186 | 1168 | .or_else(|| self.workspaces[workspace_idx].tree.first_window()) |
| 1187 | 1169 | { |
| 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 |
| 1194 | 1171 | 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 | | - } |
| 1199 | 1172 | } else { |
| 1200 | 1173 | // No windows on target monitor - clear focus and warp to monitor center |
| 1201 | 1174 | self.focused_window = None; |
@@ -1271,10 +1244,65 @@ impl WindowManager { |
| 1271 | 1244 | Ok(()) |
| 1272 | 1245 | } |
| 1273 | 1246 | |
| 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 | + |
| 1274 | 1301 | /// Switch to workspace using i3-style behavior: |
| 1275 | 1302 | /// - If workspace is visible on another monitor, focus moves to that monitor |
| 1276 | 1303 | /// - 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<()> { |
| 1278 | 1306 | if idx >= self.workspaces.len() { |
| 1279 | 1307 | return Ok(()); |
| 1280 | 1308 | } |
@@ -1304,19 +1332,21 @@ impl WindowManager { |
| 1304 | 1332 | // Update EWMH |
| 1305 | 1333 | self.conn.set_current_desktop(idx as u32)?; |
| 1306 | 1334 | |
| 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 | + } |
| 1313 | 1343 | |
| 1314 | 1344 | // Focus a window on that workspace |
| 1315 | 1345 | if let Some(window) = self.workspaces[idx].focused |
| 1316 | 1346 | .or_else(|| self.workspaces[idx].floating.last().copied()) |
| 1317 | 1347 | .or_else(|| self.workspaces[idx].tree.first_window()) |
| 1318 | 1348 | { |
| 1319 | | - self.set_focus(window, true)?; |
| 1349 | + self.set_focus(window, warp_pointer)?; |
| 1320 | 1350 | } else { |
| 1321 | 1351 | self.focused_window = None; |
| 1322 | 1352 | self.conn.set_active_window(None)?; |
@@ -1367,16 +1397,18 @@ impl WindowManager { |
| 1367 | 1397 | .or_else(|| self.workspaces[idx].floating.last().copied()) |
| 1368 | 1398 | .or_else(|| self.workspaces[idx].tree.first_window()) |
| 1369 | 1399 | { |
| 1370 | | - self.set_focus(window, true)?; |
| 1400 | + self.set_focus(window, warp_pointer)?; |
| 1371 | 1401 | } else { |
| 1372 | | - // No windows - warp to center of monitor |
| 1402 | + // No windows - warp to center of monitor (only if requested) |
| 1373 | 1403 | self.focused_window = None; |
| 1374 | 1404 | 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 | + } |
| 1380 | 1412 | } |
| 1381 | 1413 | } |
| 1382 | 1414 | |
@@ -1389,6 +1421,11 @@ impl WindowManager { |
| 1389 | 1421 | Ok(()) |
| 1390 | 1422 | } |
| 1391 | 1423 | |
| 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 | + |
| 1392 | 1429 | fn move_to_workspace(&mut self, idx: usize) -> Result<()> { |
| 1393 | 1430 | if idx >= self.workspaces.len() { |
| 1394 | 1431 | return Ok(()); |
@@ -1900,17 +1937,8 @@ impl WindowManager { |
| 1900 | 1937 | .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) |
| 1901 | 1938 | .or_else(|| self.workspaces[workspace_idx].tree.first_window()) |
| 1902 | 1939 | { |
| 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 |
| 1909 | 1941 | 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 | | - } |
| 1914 | 1942 | } else { |
| 1915 | 1943 | // No windows - warp to monitor center |
| 1916 | 1944 | self.focused_window = None; |
@@ -2021,15 +2049,8 @@ impl WindowManager { |
| 2021 | 2049 | |
| 2022 | 2050 | let next_window = floating[next_idx]; |
| 2023 | 2051 | |
| 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 | | - |
| 2031 | 2052 | // 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 |
| 2033 | 2054 | self.set_focus(next_window, true)?; |
| 2034 | 2055 | self.raise_window(next_window)?; |
| 2035 | 2056 | |
@@ -2171,6 +2192,8 @@ impl WindowManager { |
| 2171 | 2192 | match MessageType::from_u32(msg_type) { |
| 2172 | 2193 | Some(MessageType::GetWorkspaces) => { |
| 2173 | 2194 | 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); |
| 2174 | 2197 | let json = build_workspaces_json(&workspaces); |
| 2175 | 2198 | if let Some(ref mut i3_ipc) = self.i3_ipc_server { |
| 2176 | 2199 | i3_ipc.send_response(client_idx, msg_type, &json); |
@@ -2204,8 +2227,14 @@ impl WindowManager { |
| 2204 | 2227 | } |
| 2205 | 2228 | } |
| 2206 | 2229 | 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 | + }; |
| 2209 | 2238 | if let Some(ref mut i3_ipc) = self.i3_ipc_server { |
| 2210 | 2239 | i3_ipc.send_response(client_idx, msg_type, json); |
| 2211 | 2240 | } |
@@ -2220,6 +2249,12 @@ impl WindowManager { |
| 2220 | 2249 | } |
| 2221 | 2250 | } |
| 2222 | 2251 | |
| 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 | + |
| 2223 | 2258 | Ok(()) |
| 2224 | 2259 | } |
| 2225 | 2260 | |