@@ -167,6 +167,14 @@ impl WindowManager { |
| 167 | 167 | Event::EnterNotify(e) => { |
| 168 | 168 | tracing::trace!("EnterNotify for window {}", e.event); |
| 169 | 169 | } |
| 170 | + Event::RandrScreenChangeNotify(_) => { |
| 171 | + tracing::info!("RandR screen change detected, refreshing monitors"); |
| 172 | + self.refresh_monitors()?; |
| 173 | + } |
| 174 | + Event::RandrNotify(_) => { |
| 175 | + tracing::info!("RandR notify event, refreshing monitors"); |
| 176 | + self.refresh_monitors()?; |
| 177 | + } |
| 170 | 178 | _ => { |
| 171 | 179 | tracing::trace!("Unhandled event: {:?}", event); |
| 172 | 180 | } |
@@ -517,6 +525,12 @@ impl WindowManager { |
| 517 | 525 | Action::CycleFloating => { |
| 518 | 526 | self.cycle_floating()?; |
| 519 | 527 | } |
| 528 | + Action::FocusMonitor(target) => { |
| 529 | + self.focus_monitor(&target)?; |
| 530 | + } |
| 531 | + Action::MoveToMonitor(target) => { |
| 532 | + self.move_to_monitor(&target)?; |
| 533 | + } |
| 520 | 534 | Action::LuaCallback(index) => { |
| 521 | 535 | if let Err(e) = self.lua_config.execute_callback(index) { |
| 522 | 536 | tracing::error!("Lua callback error: {}", e); |
@@ -613,7 +627,39 @@ impl WindowManager { |
| 613 | 627 | } |
| 614 | 628 | |
| 615 | 629 | fn switch_workspace(&mut self, idx: usize) -> Result<()> { |
| 616 | | - if idx >= self.workspaces.len() || idx == self.focused_workspace { |
| 630 | + if idx >= self.workspaces.len() { |
| 631 | + return Ok(()); |
| 632 | + } |
| 633 | + |
| 634 | + // Find which monitor owns this workspace |
| 635 | + let target_monitor = self.monitor_idx_for_workspace(idx); |
| 636 | + |
| 637 | + // If the workspace is on a different monitor, just focus that monitor |
| 638 | + if let Some(mon_idx) = target_monitor { |
| 639 | + if mon_idx != self.focused_monitor { |
| 640 | + tracing::info!("Workspace {} is on monitor {}, switching focus", idx + 1, mon_idx); |
| 641 | + self.focused_monitor = mon_idx; |
| 642 | + self.focused_workspace = idx; |
| 643 | + self.monitors[mon_idx].active_workspace = idx; |
| 644 | + self.conn.set_current_desktop(idx as u32)?; |
| 645 | + |
| 646 | + // Focus a window on the target workspace |
| 647 | + let ws = &self.workspaces[idx]; |
| 648 | + if let Some(window) = ws.focused |
| 649 | + .or_else(|| ws.floating.last().copied()) |
| 650 | + .or_else(|| ws.tree.first_window()) |
| 651 | + { |
| 652 | + self.set_focus(window)?; |
| 653 | + } else { |
| 654 | + self.focused_window = None; |
| 655 | + } |
| 656 | + self.conn.flush()?; |
| 657 | + return Ok(()); |
| 658 | + } |
| 659 | + } |
| 660 | + |
| 661 | + // Same monitor - switch its active workspace |
| 662 | + if idx == self.focused_workspace { |
| 617 | 663 | return Ok(()); |
| 618 | 664 | } |
| 619 | 665 | |
@@ -624,8 +670,10 @@ impl WindowManager { |
| 624 | 670 | self.conn.unmap_window(window)?; |
| 625 | 671 | } |
| 626 | 672 | |
| 627 | | - // Switch workspace |
| 673 | + // Update monitor's active workspace |
| 674 | + let old_ws = self.focused_workspace; |
| 628 | 675 | self.focused_workspace = idx; |
| 676 | + self.monitors[self.focused_monitor].active_workspace = idx; |
| 629 | 677 | |
| 630 | 678 | // Update EWMH _NET_CURRENT_DESKTOP |
| 631 | 679 | self.conn.set_current_desktop(idx as u32)?; |
@@ -651,6 +699,7 @@ impl WindowManager { |
| 651 | 699 | self.focused_window = None; |
| 652 | 700 | } |
| 653 | 701 | |
| 702 | + tracing::debug!("Switched from workspace {} to {}", old_ws + 1, idx + 1); |
| 654 | 703 | self.conn.flush()?; |
| 655 | 704 | Ok(()) |
| 656 | 705 | } |
@@ -691,12 +740,12 @@ impl WindowManager { |
| 691 | 740 | // Hide the window (it's moving to another workspace) |
| 692 | 741 | self.conn.unmap_window(window)?; |
| 693 | 742 | |
| 694 | | - // Insert into target workspace |
| 743 | + // Insert into target workspace (use target monitor's geometry) |
| 695 | 744 | if is_floating { |
| 696 | 745 | self.workspaces[idx].add_floating(window); |
| 697 | 746 | } else { |
| 698 | 747 | let target_focused = self.workspaces[idx].focused; |
| 699 | | - let screen = self.screen_rect(); |
| 748 | + let screen = self.workspace_rect(idx); |
| 700 | 749 | self.workspaces[idx] |
| 701 | 750 | .tree |
| 702 | 751 | .insert_with_rect(window, target_focused, screen); |
@@ -896,6 +945,23 @@ impl WindowManager { |
| 896 | 945 | // Handle subscription in handle_ipc directly |
| 897 | 946 | Response::success(None) |
| 898 | 947 | } |
| 948 | + "focus_monitor" => { |
| 949 | + let target = args.get("target").and_then(|v| v.as_str()).unwrap_or("next"); |
| 950 | + match self.focus_monitor(target) { |
| 951 | + Ok(_) => Response::success(None), |
| 952 | + Err(e) => Response::error(e.to_string()), |
| 953 | + } |
| 954 | + } |
| 955 | + "move_to_monitor" => { |
| 956 | + let target = args.get("target").and_then(|v| v.as_str()).unwrap_or("next"); |
| 957 | + match self.move_to_monitor(target) { |
| 958 | + Ok(_) => Response::success(None), |
| 959 | + Err(e) => Response::error(e.to_string()), |
| 960 | + } |
| 961 | + } |
| 962 | + "get_monitors" => { |
| 963 | + Response::success(Some(self.get_monitors_json())) |
| 964 | + } |
| 899 | 965 | _ => Response::error(format!("Unknown command: {}", command)), |
| 900 | 966 | } |
| 901 | 967 | } |
@@ -946,6 +1012,25 @@ impl WindowManager { |
| 946 | 1012 | }) |
| 947 | 1013 | } |
| 948 | 1014 | |
| 1015 | + /// Get monitor info as JSON |
| 1016 | + fn get_monitors_json(&self) -> serde_json::Value { |
| 1017 | + serde_json::json!(self.monitors.iter().enumerate().map(|(i, mon)| { |
| 1018 | + serde_json::json!({ |
| 1019 | + "name": mon.name, |
| 1020 | + "focused": i == self.focused_monitor, |
| 1021 | + "primary": mon.primary, |
| 1022 | + "geometry": { |
| 1023 | + "x": mon.geometry.x, |
| 1024 | + "y": mon.geometry.y, |
| 1025 | + "width": mon.geometry.width, |
| 1026 | + "height": mon.geometry.height, |
| 1027 | + }, |
| 1028 | + "workspaces": mon.workspaces.iter().map(|ws| ws + 1).collect::<Vec<_>>(), |
| 1029 | + "active_workspace": mon.active_workspace + 1, |
| 1030 | + }) |
| 1031 | + }).collect::<Vec<_>>()) |
| 1032 | + } |
| 1033 | + |
| 949 | 1034 | // Floating window helpers |
| 950 | 1035 | |
| 951 | 1036 | fn is_floating(&self, window: u32) -> bool { |
@@ -1006,6 +1091,140 @@ impl WindowManager { |
| 1006 | 1091 | Ok(()) |
| 1007 | 1092 | } |
| 1008 | 1093 | |
| 1094 | + /// Focus a different monitor. |
| 1095 | + /// Target can be "next", "prev", "left", "right", or a monitor name. |
| 1096 | + fn focus_monitor(&mut self, target: &str) -> Result<()> { |
| 1097 | + if self.monitors.len() <= 1 { |
| 1098 | + return Ok(()); |
| 1099 | + } |
| 1100 | + |
| 1101 | + let target_idx = match target.to_lowercase().as_str() { |
| 1102 | + "next" | "right" => (self.focused_monitor + 1) % self.monitors.len(), |
| 1103 | + "prev" | "left" => { |
| 1104 | + if self.focused_monitor == 0 { |
| 1105 | + self.monitors.len() - 1 |
| 1106 | + } else { |
| 1107 | + self.focused_monitor - 1 |
| 1108 | + } |
| 1109 | + } |
| 1110 | + name => { |
| 1111 | + // Find monitor by name |
| 1112 | + match self.monitors.iter().position(|m| m.name.eq_ignore_ascii_case(name)) { |
| 1113 | + Some(idx) => idx, |
| 1114 | + None => { |
| 1115 | + tracing::warn!("Monitor '{}' not found", name); |
| 1116 | + return Ok(()); |
| 1117 | + } |
| 1118 | + } |
| 1119 | + } |
| 1120 | + }; |
| 1121 | + |
| 1122 | + if target_idx == self.focused_monitor { |
| 1123 | + return Ok(()); |
| 1124 | + } |
| 1125 | + |
| 1126 | + tracing::info!("Focusing monitor {}: '{}'", target_idx, self.monitors[target_idx].name); |
| 1127 | + self.focused_monitor = target_idx; |
| 1128 | + |
| 1129 | + // Focus the active workspace on that monitor |
| 1130 | + let workspace_idx = self.monitors[target_idx].active_workspace; |
| 1131 | + self.focused_workspace = workspace_idx; |
| 1132 | + |
| 1133 | + // Focus a window on that workspace if any |
| 1134 | + if let Some(window) = self.workspaces[workspace_idx].focused |
| 1135 | + .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) |
| 1136 | + .or_else(|| self.workspaces[workspace_idx].tree.first_window()) |
| 1137 | + { |
| 1138 | + if let Some(old) = self.focused_window { |
| 1139 | + self.conn.grab_button(old)?; |
| 1140 | + } |
| 1141 | + self.set_focus(window)?; |
| 1142 | + self.conn.ungrab_button(window)?; |
| 1143 | + } else { |
| 1144 | + self.focused_window = None; |
| 1145 | + } |
| 1146 | + |
| 1147 | + self.conn.flush()?; |
| 1148 | + Ok(()) |
| 1149 | + } |
| 1150 | + |
| 1151 | + /// Move focused window to another monitor. |
| 1152 | + /// Target can be "next", "prev", "left", "right", or a monitor name. |
| 1153 | + fn move_to_monitor(&mut self, target: &str) -> Result<()> { |
| 1154 | + if self.monitors.len() <= 1 { |
| 1155 | + return Ok(()); |
| 1156 | + } |
| 1157 | + |
| 1158 | + let Some(window) = self.focused_window else { |
| 1159 | + return Ok(()); |
| 1160 | + }; |
| 1161 | + |
| 1162 | + let target_idx = match target.to_lowercase().as_str() { |
| 1163 | + "next" | "right" => (self.focused_monitor + 1) % self.monitors.len(), |
| 1164 | + "prev" | "left" => { |
| 1165 | + if self.focused_monitor == 0 { |
| 1166 | + self.monitors.len() - 1 |
| 1167 | + } else { |
| 1168 | + self.focused_monitor - 1 |
| 1169 | + } |
| 1170 | + } |
| 1171 | + name => { |
| 1172 | + match self.monitors.iter().position(|m| m.name.eq_ignore_ascii_case(name)) { |
| 1173 | + Some(idx) => idx, |
| 1174 | + None => { |
| 1175 | + tracing::warn!("Monitor '{}' not found", name); |
| 1176 | + return Ok(()); |
| 1177 | + } |
| 1178 | + } |
| 1179 | + } |
| 1180 | + }; |
| 1181 | + |
| 1182 | + if target_idx == self.focused_monitor { |
| 1183 | + return Ok(()); |
| 1184 | + } |
| 1185 | + |
| 1186 | + let is_floating = self.windows.get(&window).map(|w| w.floating).unwrap_or(false); |
| 1187 | + let target_workspace = self.monitors[target_idx].active_workspace; |
| 1188 | + |
| 1189 | + tracing::info!("Moving window {} to monitor {}: '{}' (workspace {})", |
| 1190 | + window, target_idx, self.monitors[target_idx].name, target_workspace + 1); |
| 1191 | + |
| 1192 | + // Remove from current workspace |
| 1193 | + if is_floating { |
| 1194 | + self.current_workspace_mut().remove_floating(window); |
| 1195 | + } else { |
| 1196 | + self.current_workspace_mut().tree.remove(window); |
| 1197 | + } |
| 1198 | + |
| 1199 | + // Update window's workspace |
| 1200 | + if let Some(win) = self.windows.get_mut(&window) { |
| 1201 | + win.workspace = target_workspace; |
| 1202 | + } |
| 1203 | + |
| 1204 | + // Add to target workspace |
| 1205 | + if is_floating { |
| 1206 | + self.workspaces[target_workspace].add_floating(window); |
| 1207 | + } else { |
| 1208 | + let target_focused = self.workspaces[target_workspace].focused; |
| 1209 | + let target_rect = self.monitors[target_idx].geometry; |
| 1210 | + self.workspaces[target_workspace].tree.insert_with_rect(window, target_focused, target_rect); |
| 1211 | + } |
| 1212 | + |
| 1213 | + // Update EWMH |
| 1214 | + self.conn.set_window_desktop(window, target_workspace as u32)?; |
| 1215 | + |
| 1216 | + // Focus follows window to new monitor |
| 1217 | + self.focused_monitor = target_idx; |
| 1218 | + self.focused_workspace = target_workspace; |
| 1219 | + self.workspaces[target_workspace].focused = Some(window); |
| 1220 | + |
| 1221 | + // Apply layouts on both monitors |
| 1222 | + self.apply_layout()?; |
| 1223 | + |
| 1224 | + self.conn.flush()?; |
| 1225 | + Ok(()) |
| 1226 | + } |
| 1227 | + |
| 1009 | 1228 | /// Cycle through floating windows on the current workspace. |
| 1010 | 1229 | fn cycle_floating(&mut self) -> Result<()> { |
| 1011 | 1230 | let floating = &self.current_workspace().floating; |