@@ -18,6 +18,16 @@ const XK_LEFT: u32 = 0xff51; |
| 18 | 18 | const XK_UP: u32 = 0xff52; |
| 19 | 19 | const XK_RIGHT: u32 = 0xff53; |
| 20 | 20 | const XK_DOWN: u32 = 0xff54; |
| 21 | +const XK_1: u32 = 0x31; |
| 22 | +const XK_2: u32 = 0x32; |
| 23 | +const XK_3: u32 = 0x33; |
| 24 | +const XK_4: u32 = 0x34; |
| 25 | +const XK_5: u32 = 0x35; |
| 26 | +const XK_6: u32 = 0x36; |
| 27 | +const XK_7: u32 = 0x37; |
| 28 | +const XK_8: u32 = 0x38; |
| 29 | +const XK_9: u32 = 0x39; |
| 30 | +const XK_0: u32 = 0x30; |
| 21 | 31 | |
| 22 | 32 | /// Keybind action types |
| 23 | 33 | #[derive(Debug, Clone)] |
@@ -28,6 +38,8 @@ enum Action { |
| 28 | 38 | Swap(Direction), |
| 29 | 39 | Resize(Direction), |
| 30 | 40 | Equalize, |
| 41 | + SwitchWorkspace(usize), |
| 42 | + MoveToWorkspace(usize), |
| 31 | 43 | } |
| 32 | 44 | |
| 33 | 45 | struct Keybind { |
@@ -122,6 +134,28 @@ impl WindowManager { |
| 122 | 134 | keysym: XK_DOWN, |
| 123 | 135 | action: Action::Resize(Direction::Down), |
| 124 | 136 | }, |
| 137 | + // Alt+1-9,0: switch workspace |
| 138 | + Keybind { modifiers: ModMask::M1, keysym: XK_1, action: Action::SwitchWorkspace(0) }, |
| 139 | + Keybind { modifiers: ModMask::M1, keysym: XK_2, action: Action::SwitchWorkspace(1) }, |
| 140 | + Keybind { modifiers: ModMask::M1, keysym: XK_3, action: Action::SwitchWorkspace(2) }, |
| 141 | + Keybind { modifiers: ModMask::M1, keysym: XK_4, action: Action::SwitchWorkspace(3) }, |
| 142 | + Keybind { modifiers: ModMask::M1, keysym: XK_5, action: Action::SwitchWorkspace(4) }, |
| 143 | + Keybind { modifiers: ModMask::M1, keysym: XK_6, action: Action::SwitchWorkspace(5) }, |
| 144 | + Keybind { modifiers: ModMask::M1, keysym: XK_7, action: Action::SwitchWorkspace(6) }, |
| 145 | + Keybind { modifiers: ModMask::M1, keysym: XK_8, action: Action::SwitchWorkspace(7) }, |
| 146 | + Keybind { modifiers: ModMask::M1, keysym: XK_9, action: Action::SwitchWorkspace(8) }, |
| 147 | + Keybind { modifiers: ModMask::M1, keysym: XK_0, action: Action::SwitchWorkspace(9) }, |
| 148 | + // Alt+Shift+1-9,0: move window to workspace |
| 149 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_1, action: Action::MoveToWorkspace(0) }, |
| 150 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_2, action: Action::MoveToWorkspace(1) }, |
| 151 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_3, action: Action::MoveToWorkspace(2) }, |
| 152 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_4, action: Action::MoveToWorkspace(3) }, |
| 153 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_5, action: Action::MoveToWorkspace(4) }, |
| 154 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_6, action: Action::MoveToWorkspace(5) }, |
| 155 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_7, action: Action::MoveToWorkspace(6) }, |
| 156 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_8, action: Action::MoveToWorkspace(7) }, |
| 157 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_9, action: Action::MoveToWorkspace(8) }, |
| 158 | + Keybind { modifiers: ModMask::M1 | ModMask::SHIFT, keysym: XK_0, action: Action::MoveToWorkspace(9) }, |
| 125 | 159 | ] |
| 126 | 160 | } |
| 127 | 161 | |
@@ -218,15 +252,25 @@ impl WindowManager { |
| 218 | 252 | fn handle_unmap_notify(&mut self, event: UnmapNotifyEvent) -> Result<()> { |
| 219 | 253 | tracing::debug!("UnmapNotify for window {}", event.window); |
| 220 | 254 | |
| 221 | | - // Remove from management |
| 222 | | - self.unmanage_window(event.window); |
| 255 | + // Only unmanage if window is on current workspace |
| 256 | + // (windows on other workspaces are unmapped due to workspace switching) |
| 257 | + let on_current = self |
| 258 | + .windows |
| 259 | + .get(&event.window) |
| 260 | + .map(|w| w.workspace == self.focused_workspace) |
| 261 | + .unwrap_or(false); |
| 223 | 262 | |
| 224 | | - // Re-apply layout |
| 225 | | - self.apply_layout()?; |
| 263 | + if on_current { |
| 264 | + // Remove from management |
| 265 | + self.unmanage_window(event.window); |
| 226 | 266 | |
| 227 | | - // Update focus |
| 228 | | - if let Some(window) = self.focused_window { |
| 229 | | - self.set_focus(window)?; |
| 267 | + // Re-apply layout |
| 268 | + self.apply_layout()?; |
| 269 | + |
| 270 | + // Update focus |
| 271 | + if let Some(window) = self.focused_window { |
| 272 | + self.set_focus(window)?; |
| 273 | + } |
| 230 | 274 | } |
| 231 | 275 | |
| 232 | 276 | Ok(()) |
@@ -325,6 +369,12 @@ impl WindowManager { |
| 325 | 369 | Action::Equalize => { |
| 326 | 370 | self.equalize()?; |
| 327 | 371 | } |
| 372 | + Action::SwitchWorkspace(idx) => { |
| 373 | + self.switch_workspace(idx)?; |
| 374 | + } |
| 375 | + Action::MoveToWorkspace(idx) => { |
| 376 | + self.move_to_workspace(idx)?; |
| 377 | + } |
| 328 | 378 | } |
| 329 | 379 | Ok(()) |
| 330 | 380 | } |
@@ -429,6 +479,85 @@ impl WindowManager { |
| 429 | 479 | Ok(()) |
| 430 | 480 | } |
| 431 | 481 | |
| 482 | + fn switch_workspace(&mut self, idx: usize) -> Result<()> { |
| 483 | + if idx >= self.workspaces.len() || idx == self.focused_workspace { |
| 484 | + return Ok(()); |
| 485 | + } |
| 486 | + |
| 487 | + tracing::info!("Switching to workspace {}", idx + 1); |
| 488 | + |
| 489 | + // Hide windows on current workspace |
| 490 | + for window in self.current_workspace().tree.windows() { |
| 491 | + self.conn.unmap_window(window)?; |
| 492 | + } |
| 493 | + |
| 494 | + // Switch workspace |
| 495 | + self.focused_workspace = idx; |
| 496 | + |
| 497 | + // Show windows on new workspace |
| 498 | + for window in self.current_workspace().tree.windows() { |
| 499 | + self.conn.map_window(window)?; |
| 500 | + } |
| 501 | + |
| 502 | + // Apply layout and update focus |
| 503 | + self.apply_layout()?; |
| 504 | + |
| 505 | + // Focus the workspace's focused window or first window |
| 506 | + if let Some(window) = self.current_workspace().focused.or_else(|| self.current_workspace().tree.first_window()) { |
| 507 | + self.set_focus(window)?; |
| 508 | + self.conn.ungrab_button(window)?; |
| 509 | + } else { |
| 510 | + self.focused_window = None; |
| 511 | + } |
| 512 | + |
| 513 | + self.conn.flush()?; |
| 514 | + Ok(()) |
| 515 | + } |
| 516 | + |
| 517 | + fn move_to_workspace(&mut self, idx: usize) -> Result<()> { |
| 518 | + if idx >= self.workspaces.len() || idx == self.focused_workspace { |
| 519 | + return Ok(()); |
| 520 | + } |
| 521 | + |
| 522 | + let Some(window) = self.focused_window else { |
| 523 | + return Ok(()); |
| 524 | + }; |
| 525 | + |
| 526 | + tracing::info!("Moving window {} to workspace {}", window, idx + 1); |
| 527 | + |
| 528 | + // Remove from current workspace tree |
| 529 | + self.current_workspace_mut().tree.remove(window); |
| 530 | + |
| 531 | + // Update focus on current workspace |
| 532 | + self.focused_window = self.current_workspace().tree.first_window(); |
| 533 | + self.current_workspace_mut().focused = self.focused_window; |
| 534 | + |
| 535 | + // Update window's workspace tracking |
| 536 | + if let Some(win) = self.windows.get_mut(&window) { |
| 537 | + win.workspace = idx; |
| 538 | + } |
| 539 | + |
| 540 | + // Hide the window (it's moving to another workspace) |
| 541 | + self.conn.unmap_window(window)?; |
| 542 | + |
| 543 | + // Insert into target workspace |
| 544 | + let target_focused = self.workspaces[idx].focused; |
| 545 | + let screen = self.screen_rect(); |
| 546 | + self.workspaces[idx].tree.insert_with_rect(window, target_focused, screen); |
| 547 | + |
| 548 | + // Re-apply layout on current workspace |
| 549 | + self.apply_layout()?; |
| 550 | + |
| 551 | + // Update focus |
| 552 | + if let Some(new_focus) = self.focused_window { |
| 553 | + self.set_focus(new_focus)?; |
| 554 | + self.conn.ungrab_button(new_focus)?; |
| 555 | + } |
| 556 | + |
| 557 | + self.conn.flush()?; |
| 558 | + Ok(()) |
| 559 | + } |
| 560 | + |
| 432 | 561 | pub fn run(&mut self) -> Result<()> { |
| 433 | 562 | tracing::info!("Starting event loop"); |
| 434 | 563 | |