@@ -167,6 +167,14 @@ impl WindowManager { |
| 167 | Event::EnterNotify(e) => { | 167 | Event::EnterNotify(e) => { |
| 168 | tracing::trace!("EnterNotify for window {}", e.event); | 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 | tracing::trace!("Unhandled event: {:?}", event); | 179 | tracing::trace!("Unhandled event: {:?}", event); |
| 172 | } | 180 | } |
@@ -517,6 +525,12 @@ impl WindowManager { |
| 517 | Action::CycleFloating => { | 525 | Action::CycleFloating => { |
| 518 | self.cycle_floating()?; | 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 | Action::LuaCallback(index) => { | 534 | Action::LuaCallback(index) => { |
| 521 | if let Err(e) = self.lua_config.execute_callback(index) { | 535 | if let Err(e) = self.lua_config.execute_callback(index) { |
| 522 | tracing::error!("Lua callback error: {}", e); | 536 | tracing::error!("Lua callback error: {}", e); |
@@ -613,7 +627,39 @@ impl WindowManager { |
| 613 | } | 627 | } |
| 614 | | 628 | |
| 615 | fn switch_workspace(&mut self, idx: usize) -> Result<()> { | 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 | return Ok(()); | 663 | return Ok(()); |
| 618 | } | 664 | } |
| 619 | | 665 | |
@@ -624,8 +670,10 @@ impl WindowManager { |
| 624 | self.conn.unmap_window(window)?; | 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 | self.focused_workspace = idx; | 675 | self.focused_workspace = idx; |
| | 676 | + self.monitors[self.focused_monitor].active_workspace = idx; |
| 629 | | 677 | |
| 630 | // Update EWMH _NET_CURRENT_DESKTOP | 678 | // Update EWMH _NET_CURRENT_DESKTOP |
| 631 | self.conn.set_current_desktop(idx as u32)?; | 679 | self.conn.set_current_desktop(idx as u32)?; |
@@ -651,6 +699,7 @@ impl WindowManager { |
| 651 | self.focused_window = None; | 699 | self.focused_window = None; |
| 652 | } | 700 | } |
| 653 | | 701 | |
| | 702 | + tracing::debug!("Switched from workspace {} to {}", old_ws + 1, idx + 1); |
| 654 | self.conn.flush()?; | 703 | self.conn.flush()?; |
| 655 | Ok(()) | 704 | Ok(()) |
| 656 | } | 705 | } |
@@ -691,12 +740,12 @@ impl WindowManager { |
| 691 | // Hide the window (it's moving to another workspace) | 740 | // Hide the window (it's moving to another workspace) |
| 692 | self.conn.unmap_window(window)?; | 741 | self.conn.unmap_window(window)?; |
| 693 | | 742 | |
| 694 | - // Insert into target workspace | 743 | + // Insert into target workspace (use target monitor's geometry) |
| 695 | if is_floating { | 744 | if is_floating { |
| 696 | self.workspaces[idx].add_floating(window); | 745 | self.workspaces[idx].add_floating(window); |
| 697 | } else { | 746 | } else { |
| 698 | let target_focused = self.workspaces[idx].focused; | 747 | let target_focused = self.workspaces[idx].focused; |
| 699 | - let screen = self.screen_rect(); | 748 | + let screen = self.workspace_rect(idx); |
| 700 | self.workspaces[idx] | 749 | self.workspaces[idx] |
| 701 | .tree | 750 | .tree |
| 702 | .insert_with_rect(window, target_focused, screen); | 751 | .insert_with_rect(window, target_focused, screen); |
@@ -896,6 +945,23 @@ impl WindowManager { |
| 896 | // Handle subscription in handle_ipc directly | 945 | // Handle subscription in handle_ipc directly |
| 897 | Response::success(None) | 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 | _ => Response::error(format!("Unknown command: {}", command)), | 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 | // Floating window helpers | 1034 | // Floating window helpers |
| 950 | | 1035 | |
| 951 | fn is_floating(&self, window: u32) -> bool { | 1036 | fn is_floating(&self, window: u32) -> bool { |
@@ -1006,6 +1091,140 @@ impl WindowManager { |
| 1006 | Ok(()) | 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 | /// Cycle through floating windows on the current workspace. | 1228 | /// Cycle through floating windows on the current workspace. |
| 1010 | fn cycle_floating(&mut self) -> Result<()> { | 1229 | fn cycle_floating(&mut self) -> Result<()> { |
| 1011 | let floating = &self.current_workspace().floating; | 1230 | let floating = &self.current_workspace().floating; |