@@ -7,24 +7,139 @@ use x11rb::protocol::xproto::{ |
| 7 | 7 | }; |
| 8 | 8 | use x11rb::protocol::Event; |
| 9 | 9 | |
| 10 | | -use crate::core::WindowManager; |
| 10 | +use crate::core::{Direction, Node, WindowManager}; |
| 11 | 11 | use crate::Result; |
| 12 | 12 | |
| 13 | | -// XK_Return keysym |
| 13 | +// Keysym constants |
| 14 | 14 | const XK_RETURN: u32 = 0xff0d; |
| 15 | +const XK_Q: u32 = 0x71; |
| 16 | +const XK_E: u32 = 0x65; |
| 17 | +const XK_LEFT: u32 = 0xff51; |
| 18 | +const XK_UP: u32 = 0xff52; |
| 19 | +const XK_RIGHT: u32 = 0xff53; |
| 20 | +const XK_DOWN: u32 = 0xff54; |
| 21 | + |
| 22 | +/// Keybind action types |
| 23 | +#[derive(Debug, Clone)] |
| 24 | +enum Action { |
| 25 | + SpawnTerminal, |
| 26 | + CloseWindow, |
| 27 | + Focus(Direction), |
| 28 | + Swap(Direction), |
| 29 | + Resize(Direction), |
| 30 | + Equalize, |
| 31 | +} |
| 32 | + |
| 33 | +struct Keybind { |
| 34 | + modifiers: ModMask, |
| 35 | + keysym: u32, |
| 36 | + action: Action, |
| 37 | +} |
| 15 | 38 | |
| 16 | 39 | impl WindowManager { |
| 40 | + /// Get all keybinds to register. |
| 41 | + fn keybinds() -> Vec<Keybind> { |
| 42 | + vec![ |
| 43 | + // Mod+Return: spawn terminal |
| 44 | + Keybind { |
| 45 | + modifiers: ModMask::M4, |
| 46 | + keysym: XK_RETURN, |
| 47 | + action: Action::SpawnTerminal, |
| 48 | + }, |
| 49 | + // Mod+Q: close window |
| 50 | + Keybind { |
| 51 | + modifiers: ModMask::M4, |
| 52 | + keysym: XK_Q, |
| 53 | + action: Action::CloseWindow, |
| 54 | + }, |
| 55 | + // Mod+E: equalize splits |
| 56 | + Keybind { |
| 57 | + modifiers: ModMask::M4, |
| 58 | + keysym: XK_E, |
| 59 | + action: Action::Equalize, |
| 60 | + }, |
| 61 | + // Mod+Arrows: focus navigation |
| 62 | + Keybind { |
| 63 | + modifiers: ModMask::M4, |
| 64 | + keysym: XK_LEFT, |
| 65 | + action: Action::Focus(Direction::Left), |
| 66 | + }, |
| 67 | + Keybind { |
| 68 | + modifiers: ModMask::M4, |
| 69 | + keysym: XK_RIGHT, |
| 70 | + action: Action::Focus(Direction::Right), |
| 71 | + }, |
| 72 | + Keybind { |
| 73 | + modifiers: ModMask::M4, |
| 74 | + keysym: XK_UP, |
| 75 | + action: Action::Focus(Direction::Up), |
| 76 | + }, |
| 77 | + Keybind { |
| 78 | + modifiers: ModMask::M4, |
| 79 | + keysym: XK_DOWN, |
| 80 | + action: Action::Focus(Direction::Down), |
| 81 | + }, |
| 82 | + // Mod+Shift+Arrows: swap windows |
| 83 | + Keybind { |
| 84 | + modifiers: ModMask::M4 | ModMask::SHIFT, |
| 85 | + keysym: XK_LEFT, |
| 86 | + action: Action::Swap(Direction::Left), |
| 87 | + }, |
| 88 | + Keybind { |
| 89 | + modifiers: ModMask::M4 | ModMask::SHIFT, |
| 90 | + keysym: XK_RIGHT, |
| 91 | + action: Action::Swap(Direction::Right), |
| 92 | + }, |
| 93 | + Keybind { |
| 94 | + modifiers: ModMask::M4 | ModMask::SHIFT, |
| 95 | + keysym: XK_UP, |
| 96 | + action: Action::Swap(Direction::Up), |
| 97 | + }, |
| 98 | + Keybind { |
| 99 | + modifiers: ModMask::M4 | ModMask::SHIFT, |
| 100 | + keysym: XK_DOWN, |
| 101 | + action: Action::Swap(Direction::Down), |
| 102 | + }, |
| 103 | + // Mod+Ctrl+Arrows: resize |
| 104 | + Keybind { |
| 105 | + modifiers: ModMask::M4 | ModMask::CONTROL, |
| 106 | + keysym: XK_LEFT, |
| 107 | + action: Action::Resize(Direction::Left), |
| 108 | + }, |
| 109 | + Keybind { |
| 110 | + modifiers: ModMask::M4 | ModMask::CONTROL, |
| 111 | + keysym: XK_RIGHT, |
| 112 | + action: Action::Resize(Direction::Right), |
| 113 | + }, |
| 114 | + Keybind { |
| 115 | + modifiers: ModMask::M4 | ModMask::CONTROL, |
| 116 | + keysym: XK_UP, |
| 117 | + action: Action::Resize(Direction::Up), |
| 118 | + }, |
| 119 | + Keybind { |
| 120 | + modifiers: ModMask::M4 | ModMask::CONTROL, |
| 121 | + keysym: XK_DOWN, |
| 122 | + action: Action::Resize(Direction::Down), |
| 123 | + }, |
| 124 | + ] |
| 125 | + } |
| 126 | + |
| 17 | 127 | /// Set up initial keybinds and grabs. |
| 18 | 128 | pub fn setup_grabs(&mut self) -> Result<()> { |
| 19 | | - // Grab Mod4 + Return for terminal |
| 20 | | - if let Some(keycode) = self.conn.keycode_from_keysym(XK_RETURN) { |
| 21 | | - self.conn.grab_key(ModMask::M4, keycode)?; |
| 22 | | - tracing::info!("Grabbed Mod4+Return (keycode {})", keycode); |
| 23 | | - } else { |
| 24 | | - tracing::warn!("Could not find keycode for Return key"); |
| 129 | + for keybind in Self::keybinds() { |
| 130 | + if let Some(keycode) = self.conn.keycode_from_keysym(keybind.keysym) { |
| 131 | + self.conn.grab_key(keybind.modifiers, keycode)?; |
| 132 | + tracing::debug!( |
| 133 | + "Grabbed {:?}+keycode {} for {:?}", |
| 134 | + keybind.modifiers, |
| 135 | + keycode, |
| 136 | + keybind.action |
| 137 | + ); |
| 138 | + } |
| 25 | 139 | } |
| 26 | 140 | |
| 27 | 141 | self.conn.flush()?; |
| 142 | + tracing::info!("Keybinds registered"); |
| 28 | 143 | Ok(()) |
| 29 | 144 | } |
| 30 | 145 | |
@@ -95,7 +210,6 @@ impl WindowManager { |
| 95 | 210 | self.conn.flush()?; |
| 96 | 211 | } |
| 97 | 212 | // If we are managing it, we control its geometry via apply_layout() |
| 98 | | - // So we ignore the configure request (or could send a synthetic ConfigureNotify) |
| 99 | 213 | |
| 100 | 214 | Ok(()) |
| 101 | 215 | } |
@@ -167,19 +281,53 @@ impl WindowManager { |
| 167 | 281 | |
| 168 | 282 | fn handle_key_press(&mut self, event: KeyPressEvent) -> Result<()> { |
| 169 | 283 | let keycode = event.detail; |
| 170 | | - let modifiers = event.state; |
| 171 | | - tracing::debug!("KeyPress: keycode={}, modifiers={:?}", keycode, modifiers); |
| 172 | | - |
| 173 | | - // Check for Mod4 + Return |
| 174 | | - let return_keycode = self.conn.keycode_from_keysym(XK_RETURN); |
| 175 | | - if Some(keycode) == return_keycode && modifiers.contains(ModMask::M4) { |
| 176 | | - tracing::info!("Spawning terminal"); |
| 177 | | - self.spawn_terminal(); |
| 284 | + let state = event.state; |
| 285 | + |
| 286 | + // Convert KeyButMask to ModMask for comparison |
| 287 | + let modifiers = ModMask::from( |
| 288 | + (state.bits() & (ModMask::SHIFT | ModMask::CONTROL | ModMask::M1 | ModMask::M4).bits()) |
| 289 | + as u16, |
| 290 | + ); |
| 291 | + |
| 292 | + // Find matching keybind |
| 293 | + for keybind in Self::keybinds() { |
| 294 | + let bind_keycode = self.conn.keycode_from_keysym(keybind.keysym); |
| 295 | + if bind_keycode == Some(keycode) && keybind.modifiers == modifiers { |
| 296 | + tracing::debug!("Executing action: {:?}", keybind.action); |
| 297 | + self.execute_action(keybind.action)?; |
| 298 | + return Ok(()); |
| 299 | + } |
| 178 | 300 | } |
| 179 | 301 | |
| 180 | 302 | Ok(()) |
| 181 | 303 | } |
| 182 | 304 | |
| 305 | + fn execute_action(&mut self, action: Action) -> Result<()> { |
| 306 | + match action { |
| 307 | + Action::SpawnTerminal => { |
| 308 | + self.spawn_terminal(); |
| 309 | + } |
| 310 | + Action::CloseWindow => { |
| 311 | + if let Some(window) = self.focused_window { |
| 312 | + self.close_window(window)?; |
| 313 | + } |
| 314 | + } |
| 315 | + Action::Focus(direction) => { |
| 316 | + self.focus_direction(direction)?; |
| 317 | + } |
| 318 | + Action::Swap(direction) => { |
| 319 | + self.swap_direction(direction)?; |
| 320 | + } |
| 321 | + Action::Resize(direction) => { |
| 322 | + self.resize_direction(direction)?; |
| 323 | + } |
| 324 | + Action::Equalize => { |
| 325 | + self.equalize()?; |
| 326 | + } |
| 327 | + } |
| 328 | + Ok(()) |
| 329 | + } |
| 330 | + |
| 183 | 331 | fn spawn_terminal(&self) { |
| 184 | 332 | // Try common terminals in order of preference |
| 185 | 333 | let terminals = ["alacritty", "kitty", "foot", "xterm"]; |
@@ -197,6 +345,88 @@ impl WindowManager { |
| 197 | 345 | tracing::warn!("No terminal emulator found"); |
| 198 | 346 | } |
| 199 | 347 | |
| 348 | + fn close_window(&mut self, window: u32) -> Result<()> { |
| 349 | + tracing::info!("Closing window {}", window); |
| 350 | + |
| 351 | + // TODO: Send WM_DELETE_WINDOW if supported (ICCCM) |
| 352 | + // For now, just kill the client |
| 353 | + self.conn.conn.kill_client(window)?; |
| 354 | + self.conn.flush()?; |
| 355 | + |
| 356 | + Ok(()) |
| 357 | + } |
| 358 | + |
| 359 | + fn focus_direction(&mut self, direction: Direction) -> Result<()> { |
| 360 | + let Some(focused) = self.focused_window else { |
| 361 | + return Ok(()); |
| 362 | + }; |
| 363 | + |
| 364 | + let screen = self.screen_rect(); |
| 365 | + let geometries = self.current_workspace().tree.calculate_geometries(screen); |
| 366 | + |
| 367 | + if let Some(target) = Node::find_adjacent(&geometries, focused, direction) { |
| 368 | + // Regrab button on old window |
| 369 | + self.conn.grab_button(focused)?; |
| 370 | + |
| 371 | + // Focus new window |
| 372 | + self.set_focus(target)?; |
| 373 | + self.conn.ungrab_button(target)?; |
| 374 | + |
| 375 | + tracing::debug!("Focused {:?} to window {}", direction, target); |
| 376 | + } |
| 377 | + |
| 378 | + Ok(()) |
| 379 | + } |
| 380 | + |
| 381 | + fn swap_direction(&mut self, direction: Direction) -> Result<()> { |
| 382 | + let Some(focused) = self.focused_window else { |
| 383 | + return Ok(()); |
| 384 | + }; |
| 385 | + |
| 386 | + let screen = self.screen_rect(); |
| 387 | + let geometries = self.current_workspace().tree.calculate_geometries(screen); |
| 388 | + |
| 389 | + if let Some(target) = Node::find_adjacent(&geometries, focused, direction) { |
| 390 | + // Swap the windows in the tree |
| 391 | + self.current_workspace_mut().tree.swap(focused, target); |
| 392 | + |
| 393 | + // Re-apply layout |
| 394 | + self.apply_layout()?; |
| 395 | + |
| 396 | + tracing::debug!("Swapped with window {} in direction {:?}", target, direction); |
| 397 | + } |
| 398 | + |
| 399 | + Ok(()) |
| 400 | + } |
| 401 | + |
| 402 | + fn resize_direction(&mut self, direction: Direction) -> Result<()> { |
| 403 | + let Some(focused) = self.focused_window else { |
| 404 | + return Ok(()); |
| 405 | + }; |
| 406 | + |
| 407 | + const RESIZE_DELTA: f32 = 0.05; |
| 408 | + |
| 409 | + // Resize the split |
| 410 | + if self |
| 411 | + .current_workspace_mut() |
| 412 | + .tree |
| 413 | + .resize(focused, direction, RESIZE_DELTA) |
| 414 | + { |
| 415 | + // Re-apply layout |
| 416 | + self.apply_layout()?; |
| 417 | + tracing::debug!("Resized {:?}", direction); |
| 418 | + } |
| 419 | + |
| 420 | + Ok(()) |
| 421 | + } |
| 422 | + |
| 423 | + fn equalize(&mut self) -> Result<()> { |
| 424 | + self.current_workspace_mut().tree.equalize(); |
| 425 | + self.apply_layout()?; |
| 426 | + tracing::debug!("Equalized splits"); |
| 427 | + Ok(()) |
| 428 | + } |
| 429 | + |
| 200 | 430 | pub fn run(&mut self) -> Result<()> { |
| 201 | 431 | tracing::info!("Starting event loop"); |
| 202 | 432 | |