gardesk/gar / a223418

Browse files

feat: add smart splits and keyboard navigation

- Smart split: auto-detect direction based on container size
- Mod+Arrows: focus, Mod+Shift+Arrows: swap, Mod+Ctrl+Arrows: resize
- Mod+Q: close window, Mod+E: equalize splits
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
a22341805ee36db64fd93992b794a5b6f4f1fc4c
Parents
9cd4c27
Tree
41e3716

1 changed file

StatusFile+-
M gar/src/x11/events.rs 247 17
gar/src/x11/events.rsmodified
@@ -7,24 +7,139 @@ use x11rb::protocol::xproto::{
77
 };
88
 use x11rb::protocol::Event;
99
 
10
-use crate::core::WindowManager;
10
+use crate::core::{Direction, Node, WindowManager};
1111
 use crate::Result;
1212
 
13
-// XK_Return keysym
13
+// Keysym constants
1414
 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
+}
1538
 
1639
 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
+
17127
     /// Set up initial keybinds and grabs.
18128
     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
+            }
25139
         }
26140
 
27141
         self.conn.flush()?;
142
+        tracing::info!("Keybinds registered");
28143
         Ok(())
29144
     }
30145
 
@@ -95,7 +210,6 @@ impl WindowManager {
95210
             self.conn.flush()?;
96211
         }
97212
         // 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)
99213
 
100214
         Ok(())
101215
     }
@@ -167,19 +281,53 @@ impl WindowManager {
167281
 
168282
     fn handle_key_press(&mut self, event: KeyPressEvent) -> Result<()> {
169283
         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
+            }
178300
         }
179301
 
180302
         Ok(())
181303
     }
182304
 
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
+
183331
     fn spawn_terminal(&self) {
184332
         // Try common terminals in order of preference
185333
         let terminals = ["alacritty", "kitty", "foot", "xterm"];
@@ -197,6 +345,88 @@ impl WindowManager {
197345
         tracing::warn!("No terminal emulator found");
198346
     }
199347
 
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
+
200430
     pub fn run(&mut self) -> Result<()> {
201431
         tracing::info!("Starting event loop");
202432