@@ -3,8 +3,8 @@ use std::process::Command; |
| 3 | 3 | use x11rb::connection::Connection as X11Connection; |
| 4 | 4 | use x11rb::protocol::xproto::{ |
| 5 | 5 | ButtonPressEvent, ButtonReleaseEvent, ConfigureRequestEvent, ConfigureWindowAux, ConnectionExt, |
| 6 | | - DestroyNotifyEvent, EventMask, KeyPressEvent, MapRequestEvent, ModMask, MotionNotifyEvent, |
| 7 | | - StackMode, UnmapNotifyEvent, |
| 6 | + DestroyNotifyEvent, EnterNotifyEvent, EventMask, KeyPressEvent, MapRequestEvent, ModMask, |
| 7 | + MotionNotifyEvent, NotifyMode, StackMode, UnmapNotifyEvent, |
| 8 | 8 | }; |
| 9 | 9 | use x11rb::protocol::Event; |
| 10 | 10 | |
@@ -165,7 +165,7 @@ impl WindowManager { |
| 165 | 165 | Event::MotionNotify(e) => self.handle_motion_notify(e)?, |
| 166 | 166 | Event::KeyPress(e) => self.handle_key_press(e)?, |
| 167 | 167 | Event::EnterNotify(e) => { |
| 168 | | - tracing::trace!("EnterNotify for window {}", e.event); |
| 168 | + self.handle_enter_notify(e)?; |
| 169 | 169 | } |
| 170 | 170 | Event::RandrScreenChangeNotify(_) => { |
| 171 | 171 | tracing::info!("RandR screen change detected, refreshing monitors"); |
@@ -438,6 +438,56 @@ impl WindowManager { |
| 438 | 438 | Ok(()) |
| 439 | 439 | } |
| 440 | 440 | |
| 441 | + fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> { |
| 442 | + let window = event.event; |
| 443 | + |
| 444 | + // Ignore if we're in a drag operation |
| 445 | + if self.drag_state.is_some() { |
| 446 | + return Ok(()); |
| 447 | + } |
| 448 | + |
| 449 | + // Suppress EnterNotify events that happen shortly after a pointer warp |
| 450 | + // This prevents feedback loops from mouse-follows-focus |
| 451 | + if self.last_warp.elapsed() < std::time::Duration::from_millis(50) { |
| 452 | + return Ok(()); |
| 453 | + } |
| 454 | + |
| 455 | + // Ignore inferior (entering from a child window) and non-normal modes |
| 456 | + // Only handle "Normal" mode enters (actual mouse movement) |
| 457 | + if event.mode != NotifyMode::NORMAL { |
| 458 | + return Ok(()); |
| 459 | + } |
| 460 | + |
| 461 | + // Only focus windows we manage |
| 462 | + if !self.windows.contains_key(&window) { |
| 463 | + return Ok(()); |
| 464 | + } |
| 465 | + |
| 466 | + // Don't focus if already focused |
| 467 | + if self.focused_window == Some(window) { |
| 468 | + return Ok(()); |
| 469 | + } |
| 470 | + |
| 471 | + tracing::debug!("Focus follows mouse: focusing window {}", window); |
| 472 | + |
| 473 | + // Regrab button on old focused window |
| 474 | + if let Some(old) = self.focused_window { |
| 475 | + self.conn.grab_button(old)?; |
| 476 | + } |
| 477 | + |
| 478 | + // Focus the new window (this will warp pointer back, but we'll suppress the resulting EnterNotify) |
| 479 | + self.set_focus(window)?; |
| 480 | + self.conn.ungrab_button(window)?; |
| 481 | + |
| 482 | + // Raise floating windows on focus |
| 483 | + if self.is_floating(window) { |
| 484 | + self.raise_window(window)?; |
| 485 | + } |
| 486 | + |
| 487 | + self.conn.flush()?; |
| 488 | + Ok(()) |
| 489 | + } |
| 490 | + |
| 441 | 491 | fn handle_key_press(&mut self, event: KeyPressEvent) -> Result<()> { |
| 442 | 492 | let keycode = event.detail; |
| 443 | 493 | let state = event.state; |
@@ -484,6 +534,11 @@ impl WindowManager { |
| 484 | 534 | self.close_window(window)?; |
| 485 | 535 | } |
| 486 | 536 | } |
| 537 | + Action::ForceCloseWindow => { |
| 538 | + if let Some(window) = self.focused_window { |
| 539 | + self.force_close_window(window)?; |
| 540 | + } |
| 541 | + } |
| 487 | 542 | Action::Focus(direction) => { |
| 488 | 543 | if let Some(dir) = parse_direction(&direction) { |
| 489 | 544 | self.focus_direction(dir)?; |
@@ -541,14 +596,20 @@ impl WindowManager { |
| 541 | 596 | } |
| 542 | 597 | |
| 543 | 598 | fn close_window(&mut self, window: u32) -> Result<()> { |
| 599 | + // Verify we actually have a window to close |
| 600 | + if !self.windows.contains_key(&window) { |
| 601 | + tracing::warn!("close_window called on unmanaged window {}", window); |
| 602 | + return Ok(()); |
| 603 | + } |
| 604 | + |
| 544 | 605 | tracing::info!("Closing window {}", window); |
| 545 | 606 | |
| 546 | 607 | // Try graceful ICCCM close first |
| 547 | 608 | if self.conn.supports_delete_window(window) { |
| 548 | | - tracing::debug!("Window {} supports WM_DELETE_WINDOW, sending graceful close", window); |
| 609 | + tracing::info!("Window {} supports WM_DELETE_WINDOW, sending graceful close", window); |
| 549 | 610 | self.conn.send_delete_window(window)?; |
| 550 | 611 | } else { |
| 551 | | - tracing::debug!("Window {} doesn't support WM_DELETE_WINDOW, using kill_client", window); |
| 612 | + tracing::info!("Window {} doesn't support WM_DELETE_WINDOW, using kill_client", window); |
| 552 | 613 | self.conn.conn.kill_client(window)?; |
| 553 | 614 | } |
| 554 | 615 | |
@@ -556,9 +617,17 @@ impl WindowManager { |
| 556 | 617 | Ok(()) |
| 557 | 618 | } |
| 558 | 619 | |
| 620 | + fn force_close_window(&mut self, window: u32) -> Result<()> { |
| 621 | + tracing::info!("Force closing window {}", window); |
| 622 | + self.conn.conn.kill_client(window)?; |
| 623 | + self.conn.flush()?; |
| 624 | + Ok(()) |
| 625 | + } |
| 626 | + |
| 559 | 627 | fn focus_direction(&mut self, direction: Direction) -> Result<()> { |
| 560 | 628 | let Some(focused) = self.focused_window else { |
| 561 | | - return Ok(()); |
| 629 | + // No focused window - try to focus adjacent monitor |
| 630 | + return self.focus_adjacent_monitor(direction); |
| 562 | 631 | }; |
| 563 | 632 | |
| 564 | 633 | let screen = self.screen_rect(); |
@@ -574,8 +643,65 @@ impl WindowManager { |
| 574 | 643 | self.conn.flush()?; |
| 575 | 644 | |
| 576 | 645 | tracing::debug!("Focused {:?} to window {}", direction, target); |
| 646 | + } else { |
| 647 | + // No adjacent window on this workspace - try adjacent monitor |
| 648 | + self.focus_adjacent_monitor(direction)?; |
| 649 | + } |
| 650 | + |
| 651 | + Ok(()) |
| 652 | + } |
| 653 | + |
| 654 | + /// Focus the adjacent monitor in the given direction (does NOT wrap at edges) |
| 655 | + fn focus_adjacent_monitor(&mut self, direction: Direction) -> Result<()> { |
| 656 | + if self.monitors.len() <= 1 { |
| 657 | + return Ok(()); |
| 658 | + } |
| 659 | + |
| 660 | + // Calculate target index WITHOUT wrapping |
| 661 | + let target_idx = match direction { |
| 662 | + Direction::Left => { |
| 663 | + if self.focused_monitor == 0 { |
| 664 | + // At leftmost monitor - do nothing |
| 665 | + return Ok(()); |
| 666 | + } |
| 667 | + self.focused_monitor - 1 |
| 668 | + } |
| 669 | + Direction::Right => { |
| 670 | + if self.focused_monitor >= self.monitors.len() - 1 { |
| 671 | + // At rightmost monitor - do nothing |
| 672 | + return Ok(()); |
| 673 | + } |
| 674 | + self.focused_monitor + 1 |
| 675 | + } |
| 676 | + // Up/Down could navigate if monitors are stacked vertically |
| 677 | + Direction::Up | Direction::Down => return Ok(()), |
| 678 | + }; |
| 679 | + |
| 680 | + tracing::info!("Moving focus from monitor {} to {}", self.focused_monitor, target_idx); |
| 681 | + self.focused_monitor = target_idx; |
| 682 | + |
| 683 | + // Focus the active workspace on that monitor |
| 684 | + let workspace_idx = self.monitors[target_idx].active_workspace; |
| 685 | + self.focused_workspace = workspace_idx; |
| 686 | + |
| 687 | + // Focus a window on that workspace if any, or just warp to monitor center |
| 688 | + if let Some(window) = self.workspaces[workspace_idx].focused |
| 689 | + .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) |
| 690 | + .or_else(|| self.workspaces[workspace_idx].tree.first_window()) |
| 691 | + { |
| 692 | + if let Some(old) = self.focused_window { |
| 693 | + self.conn.grab_button(old)?; |
| 694 | + } |
| 695 | + self.set_focus(window)?; |
| 696 | + self.conn.ungrab_button(window)?; |
| 697 | + } else { |
| 698 | + // No windows on target monitor - clear focus and warp to monitor center |
| 699 | + self.focused_window = None; |
| 700 | + self.warp_to_monitor(target_idx)?; |
| 701 | + tracing::debug!("No windows on monitor {}, warped to center", target_idx); |
| 577 | 702 | } |
| 578 | 703 | |
| 704 | + self.conn.flush()?; |
| 579 | 705 | Ok(()) |
| 580 | 706 | } |
| 581 | 707 | |
@@ -632,66 +758,49 @@ impl WindowManager { |
| 632 | 758 | } |
| 633 | 759 | |
| 634 | 760 | // 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 | | - } |
| 761 | + let target_monitor_idx = self.monitor_idx_for_workspace(idx).unwrap_or(0); |
| 762 | + let old_ws_on_target = self.monitors[target_monitor_idx].active_workspace; |
| 660 | 763 | |
| 661 | | - // Same monitor - switch its active workspace |
| 662 | | - if idx == self.focused_workspace { |
| 764 | + // Already on this workspace? |
| 765 | + if idx == old_ws_on_target && target_monitor_idx == self.focused_monitor { |
| 663 | 766 | return Ok(()); |
| 664 | 767 | } |
| 665 | 768 | |
| 666 | | - tracing::info!("Switching to workspace {}", idx + 1); |
| 769 | + tracing::info!( |
| 770 | + "Switching to workspace {} on monitor {} (was ws {})", |
| 771 | + idx + 1, target_monitor_idx, old_ws_on_target + 1 |
| 772 | + ); |
| 773 | + |
| 774 | + // If switching to a different workspace on the target monitor, hide old/show new |
| 775 | + if idx != old_ws_on_target { |
| 776 | + // Hide windows on old workspace |
| 777 | + for window in self.workspaces[old_ws_on_target].all_windows() { |
| 778 | + self.conn.unmap_window(window)?; |
| 779 | + } |
| 667 | 780 | |
| 668 | | - // Hide all windows on current workspace (tiled + floating) |
| 669 | | - for window in self.current_workspace().all_windows() { |
| 670 | | - self.conn.unmap_window(window)?; |
| 781 | + // Update monitor's active workspace |
| 782 | + self.monitors[target_monitor_idx].active_workspace = idx; |
| 783 | + |
| 784 | + // Show windows on new workspace |
| 785 | + for window in self.workspaces[idx].all_windows() { |
| 786 | + self.conn.map_window(window)?; |
| 787 | + } |
| 671 | 788 | } |
| 672 | 789 | |
| 673 | | - // Update monitor's active workspace |
| 674 | | - let old_ws = self.focused_workspace; |
| 790 | + // Update focused state |
| 791 | + self.focused_monitor = target_monitor_idx; |
| 675 | 792 | self.focused_workspace = idx; |
| 676 | | - self.monitors[self.focused_monitor].active_workspace = idx; |
| 677 | 793 | |
| 678 | 794 | // Update EWMH _NET_CURRENT_DESKTOP |
| 679 | 795 | self.conn.set_current_desktop(idx as u32)?; |
| 680 | 796 | |
| 681 | | - // Show all windows on new workspace (tiled + floating) |
| 682 | | - for window in self.current_workspace().all_windows() { |
| 683 | | - self.conn.map_window(window)?; |
| 684 | | - } |
| 685 | | - |
| 686 | | - // Apply layout and update focus |
| 797 | + // Apply layout |
| 687 | 798 | self.apply_layout()?; |
| 688 | 799 | |
| 689 | | - // Focus the workspace's focused window, preferring floating on top |
| 690 | | - if let Some(window) = self |
| 691 | | - .current_workspace() |
| 692 | | - .focused |
| 693 | | - .or_else(|| self.current_workspace().floating.last().copied()) |
| 694 | | - .or_else(|| self.current_workspace().tree.first_window()) |
| 800 | + // Focus a window on the target workspace |
| 801 | + if let Some(window) = self.workspaces[idx].focused |
| 802 | + .or_else(|| self.workspaces[idx].floating.last().copied()) |
| 803 | + .or_else(|| self.workspaces[idx].tree.first_window()) |
| 695 | 804 | { |
| 696 | 805 | self.set_focus(window)?; |
| 697 | 806 | self.conn.ungrab_button(window)?; |
@@ -699,7 +808,6 @@ impl WindowManager { |
| 699 | 808 | self.focused_window = None; |
| 700 | 809 | } |
| 701 | 810 | |
| 702 | | - tracing::debug!("Switched from workspace {} to {}", old_ws + 1, idx + 1); |
| 703 | 811 | self.conn.flush()?; |
| 704 | 812 | Ok(()) |
| 705 | 813 | } |
@@ -1130,7 +1238,7 @@ impl WindowManager { |
| 1130 | 1238 | let workspace_idx = self.monitors[target_idx].active_workspace; |
| 1131 | 1239 | self.focused_workspace = workspace_idx; |
| 1132 | 1240 | |
| 1133 | | - // Focus a window on that workspace if any |
| 1241 | + // Focus a window on that workspace if any, or warp to monitor center |
| 1134 | 1242 | if let Some(window) = self.workspaces[workspace_idx].focused |
| 1135 | 1243 | .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) |
| 1136 | 1244 | .or_else(|| self.workspaces[workspace_idx].tree.first_window()) |
@@ -1141,7 +1249,9 @@ impl WindowManager { |
| 1141 | 1249 | self.set_focus(window)?; |
| 1142 | 1250 | self.conn.ungrab_button(window)?; |
| 1143 | 1251 | } else { |
| 1252 | + // No windows - warp to monitor center |
| 1144 | 1253 | self.focused_window = None; |
| 1254 | + self.warp_to_monitor(target_idx)?; |
| 1145 | 1255 | } |
| 1146 | 1256 | |
| 1147 | 1257 | self.conn.flush()?; |