oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git

added grid layout and positional movements. turns out grid layout was easy to add thanks to rusts based idiommatic enum. and positional movements is a solved problem (peeked leftwm for source)

Commit
026dcd91d48373a8d61bc08b752fcf59f06a4bbd
Parent
c9efeb4
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-11-07 04:59:03

Diff

diff --git a/resources/test-config.ron b/resources/test-config.ron
index 9f8b939..c8bf158 100644
--- a/resources/test-config.ron
+++ b/resources/test-config.ron
@@ -55,17 +55,16 @@
         (modifiers: [Mod1], key: A, action: ToggleGaps),
         (modifiers: [Mod1, Shift], key: Q, action: Quit),
         (modifiers: [Mod1, Shift], key: R, action: Restart),
-        (modifiers: [Mod1], key: J, action: FocusStack, arg: -1),
-        (modifiers: [Mod1], key: K, action: FocusStack, arg: 1),
-        // Exchange client (vim-style with Mod+Shift)
-        (modifiers: [Mod1, Control], key: K, action: SmartMoveWin, arg: 0), // UP
-        (modifiers: [Mod1, Control], key: J, action: SmartMoveWin, arg: 1), // DOWN
-        (modifiers: [Mod1, Control], key: H, action: SmartMoveWin, arg: 2), // LEFT
-        (modifiers: [Mod1, Control], key: L, action: SmartMoveWin, arg: 3), // RIGHT
-        (modifiers: [Mod1, Shift], key: K, action: ExchangeClient, arg: 0), // UP
-        (modifiers: [Mod1, Shift], key: J, action: ExchangeClient, arg: 1), // DOWN
-        (modifiers: [Mod1, Shift], key: H, action: ExchangeClient, arg: 2), // LEFT
-        (modifiers: [Mod1, Shift], key: L, action: ExchangeClient, arg: 3), // RIGHT
+        // (modifiers: [Mod1], key: J, action: FocusStack, arg: -1),
+        // (modifiers: [Mod1], key: K, action: FocusStack, arg: 1),
+        (modifiers: [Mod1], key: H, action: FocusDirection, arg: 2),
+        (modifiers: [Mod1], key: J, action: FocusDirection, arg: 1),
+        (modifiers: [Mod1], key: K, action: FocusDirection, arg: 0),
+        (modifiers: [Mod1], key: L, action: FocusDirection, arg: 3),
+        (modifiers: [Mod1, Shift], key: H, action: SwapDirection, arg: 2),
+        (modifiers: [Mod1, Shift], key: J, action: SwapDirection, arg: 1),
+        (modifiers: [Mod1, Shift], key: K, action: SwapDirection, arg: 0),
+        (modifiers: [Mod1, Shift], key: L, action: SwapDirection, arg: 3),
         (modifiers: [Mod1], key: Key1, action: ViewTag, arg: 0),
         (modifiers: [Mod1], key: Key2, action: ViewTag, arg: 1),
         (modifiers: [Mod1], key: Key3, action: ViewTag, arg: 2),
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 226303d..2d6e0d3 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -14,6 +14,8 @@ pub enum KeyAction {
     Spawn,
     KillClient,
     FocusStack,
+    FocusDirection,
+    SwapDirection,
     Quit,
     Restart,
     Recompile,
diff --git a/src/layout/grid.rs b/src/layout/grid.rs
new file mode 100644
index 0000000..8df9d21
--- /dev/null
+++ b/src/layout/grid.rs
@@ -0,0 +1,92 @@
+use super::{GapConfig, Layout, WindowGeometry};
+use x11rb::protocol::xproto::Window;
+
+pub struct GridLayout;
+
+impl Layout for GridLayout {
+    fn name(&self) -> &'static str {
+        super::LayoutType::Grid.as_str()
+    }
+
+    fn symbol(&self) -> &'static str {
+        "[#]"
+    }
+
+    fn arrange(
+        &self,
+        windows: &[Window],
+        screen_width: u32,
+        screen_height: u32,
+        gaps: &GapConfig,
+    ) -> Vec<WindowGeometry> {
+        let window_count = windows.len();
+        if window_count == 0 {
+            return Vec::new();
+        }
+
+        // Single window takes full screen
+        if window_count == 1 {
+            let x = gaps.outer_horizontal as i32;
+            let y = gaps.outer_vertical as i32;
+            let width = screen_width.saturating_sub(2 * gaps.outer_horizontal);
+            let height = screen_height.saturating_sub(2 * gaps.outer_vertical);
+
+            return vec![WindowGeometry {
+                x_coordinate: x,
+                y_coordinate: y,
+                width,
+                height,
+            }];
+        }
+
+        // Calculate grid dimensions using "favor rows" approach
+        // cols = ceil(sqrt(n))
+        // rows = ceil(n / cols)
+        let cols = (window_count as f64).sqrt().ceil() as usize;
+        let rows = (window_count as f64 / cols as f64).ceil() as usize;
+
+        let mut geometries = Vec::new();
+
+        // Calculate dimensions with gaps
+        let total_horizontal_gaps = gaps.outer_horizontal * 2 + gaps.inner_horizontal * (cols as u32 - 1);
+        let total_vertical_gaps = gaps.outer_vertical * 2 + gaps.inner_vertical * (rows as u32 - 1);
+
+        let cell_width = screen_width.saturating_sub(total_horizontal_gaps) / cols as u32;
+        let cell_height = screen_height.saturating_sub(total_vertical_gaps) / rows as u32;
+
+        for (index, _window) in windows.iter().enumerate() {
+            let row = index / cols;
+            let col = index % cols;
+
+            // Check if this is the last row
+            let is_last_row = row == rows - 1;
+            let windows_in_last_row = window_count - (rows - 1) * cols;
+
+            let (x, y, width, height) = if is_last_row && windows_in_last_row < cols {
+                // Last row with fewer windows - make them wider
+                let last_row_col = index % cols;
+                let last_row_cell_width = screen_width.saturating_sub(total_horizontal_gaps.saturating_sub(gaps.inner_horizontal * (cols as u32 - windows_in_last_row as u32))) / windows_in_last_row as u32;
+
+                let x = gaps.outer_horizontal + last_row_col as u32 * (last_row_cell_width + gaps.inner_horizontal);
+                let y = gaps.outer_vertical + row as u32 * (cell_height + gaps.inner_vertical);
+
+                (x as i32, y as i32, last_row_cell_width, cell_height)
+            } else {
+                // Normal grid cell
+                let x = gaps.outer_horizontal + col as u32 * (cell_width + gaps.inner_horizontal);
+                let y = gaps.outer_vertical + row as u32 * (cell_height + gaps.inner_vertical);
+
+                (x as i32, y as i32, cell_width, cell_height)
+            };
+
+            geometries.push(WindowGeometry {
+                x_coordinate: x,
+                y_coordinate: y,
+                width,
+                height,
+            });
+        }
+
+        geometries
+    }
+}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 1afb0c5..eaac7d0 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,3 +1,4 @@
+pub mod grid;
 pub mod normie;
 pub mod tiling;
 
@@ -15,6 +16,7 @@ pub struct GapConfig {
 pub enum LayoutType {
     Tiling,
     Normie,
+    Grid,
 }
 
 impl LayoutType {
@@ -22,13 +24,15 @@ impl LayoutType {
         match self {
             Self::Tiling => Box::new(tiling::TilingLayout),
             Self::Normie => Box::new(normie::NormieLayout),
+            Self::Grid => Box::new(grid::GridLayout),
         }
     }
 
     pub fn next(&self) -> Self {
         match self {
             Self::Tiling => Self::Normie,
-            Self::Normie => Self::Tiling,
+            Self::Normie => Self::Grid,
+            Self::Grid => Self::Tiling,
         }
     }
 
@@ -36,6 +40,7 @@ impl LayoutType {
         match self {
             Self::Tiling => "tiling",
             Self::Normie => "normie",
+            Self::Grid => "grid",
         }
     }
 
@@ -43,6 +48,7 @@ impl LayoutType {
         match s.to_lowercase().as_str() {
             "tiling" => Ok(Self::Tiling),
             "normie" | "floating" => Ok(Self::Normie),
+            "grid" => Ok(Self::Grid),
             _ => Err(format!("Invalid Layout Type: {}", s)),
         }
     }
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 52e12ee..760908b 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -977,6 +977,16 @@ impl WindowManager {
                     self.cycle_focus(*direction)?;
                 }
             }
+            KeyAction::FocusDirection => {
+                if let Arg::Int(direction) = arg {
+                    self.focus_direction(*direction)?;
+                }
+            }
+            KeyAction::SwapDirection => {
+                if let Arg::Int(direction) = arg {
+                    self.swap_direction(*direction)?;
+                }
+            }
             KeyAction::Quit | KeyAction::Restart => {
                 // Handled in handle_event
             }
@@ -1205,6 +1215,148 @@ impl WindowManager {
         Ok(())
     }
 
+    pub fn focus_direction(&mut self, direction: i32) -> WmResult<()> {
+        let focused = match self
+            .monitors
+            .get(self.selected_monitor)
+            .and_then(|m| m.focused_window)
+        {
+            Some(win) => win,
+            None => return Ok(()),
+        };
+
+        let visible = self.visible_windows();
+        if visible.len() < 2 {
+            return Ok(());
+        }
+
+        let focused_geom = match self.connection.get_geometry(focused)?.reply() {
+            Ok(geom) => geom,
+            Err(_) => return Ok(()),
+        };
+
+        let focused_center_x = focused_geom.x + (focused_geom.width as i16 / 2);
+        let focused_center_y = focused_geom.y + (focused_geom.height as i16 / 2);
+
+        let mut candidates = Vec::new();
+
+        for &window in &visible {
+            if window == focused {
+                continue;
+            }
+
+            let geom = match self.connection.get_geometry(window)?.reply() {
+                Ok(g) => g,
+                Err(_) => continue,
+            };
+
+            let center_x = geom.x + (geom.width as i16 / 2);
+            let center_y = geom.y + (geom.height as i16 / 2);
+
+            let is_valid_direction = match direction {
+                0 => center_y < focused_center_y,
+                1 => center_y > focused_center_y,
+                2 => center_x < focused_center_x,
+                3 => center_x > focused_center_x,
+                _ => false,
+            };
+
+            if is_valid_direction {
+                let dx = (center_x - focused_center_x) as i32;
+                let dy = (center_y - focused_center_y) as i32;
+                let distance = dx * dx + dy * dy;
+                candidates.push((window, distance));
+            }
+        }
+
+        if let Some(&(closest_window, _)) = candidates.iter().min_by_key(|&(_, dist)| dist) {
+            self.set_focus(closest_window)?;
+        }
+
+        Ok(())
+    }
+
+    pub fn swap_direction(&mut self, direction: i32) -> WmResult<()> {
+        let focused = match self
+            .monitors
+            .get(self.selected_monitor)
+            .and_then(|m| m.focused_window)
+        {
+            Some(win) => win,
+            None => return Ok(()),
+        };
+
+        let visible = self.visible_windows();
+        if visible.len() < 2 {
+            return Ok(());
+        }
+
+        let focused_geom = match self.connection.get_geometry(focused)?.reply() {
+            Ok(geom) => geom,
+            Err(_) => return Ok(()),
+        };
+
+        let focused_center_x = focused_geom.x + (focused_geom.width as i16 / 2);
+        let focused_center_y = focused_geom.y + (focused_geom.height as i16 / 2);
+
+        let mut candidates = Vec::new();
+
+        for &window in &visible {
+            if window == focused {
+                continue;
+            }
+
+            let geom = match self.connection.get_geometry(window)?.reply() {
+                Ok(g) => g,
+                Err(_) => continue,
+            };
+
+            let center_x = geom.x + (geom.width as i16 / 2);
+            let center_y = geom.y + (geom.height as i16 / 2);
+
+            let is_valid_direction = match direction {
+                0 => center_y < focused_center_y,
+                1 => center_y > focused_center_y,
+                2 => center_x < focused_center_x,
+                3 => center_x > focused_center_x,
+                _ => false,
+            };
+
+            if is_valid_direction {
+                let dx = (center_x - focused_center_x) as i32;
+                let dy = (center_y - focused_center_y) as i32;
+                let distance = dx * dx + dy * dy;
+                candidates.push((window, distance));
+            }
+        }
+
+        if let Some(&(target_window, _)) = candidates.iter().min_by_key(|&(_, dist)| dist) {
+            let focused_pos = self.windows.iter().position(|&w| w == focused);
+            let target_pos = self.windows.iter().position(|&w| w == target_window);
+
+            if let (Some(f_pos), Some(t_pos)) = (focused_pos, target_pos) {
+                self.windows.swap(f_pos, t_pos);
+                self.apply_layout()?;
+                self.set_focus(focused)?;
+
+                if let Ok(geometry) = self.connection.get_geometry(focused)?.reply() {
+                    self.connection.warp_pointer(
+                        x11rb::NONE,
+                        focused,
+                        0,
+                        0,
+                        0,
+                        0,
+                        geometry.width as i16 / 2,
+                        geometry.height as i16 / 2,
+                    )?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+
     fn grab_next_keys(&self, candidates: &[usize], keys_pressed: usize) -> WmResult<()> {
         use std::collections::HashMap;
         use x11rb::protocol::xproto::Keycode;
diff --git a/templates/config.ron b/templates/config.ron
index a770fa7..d836e33 100644
--- a/templates/config.ron
+++ b/templates/config.ron
@@ -66,8 +66,10 @@
         (modifiers: [$modkey], key: A, action: ToggleGaps),
         (modifiers: [$modkey, Shift], key: Q, action: Quit),
         (modifiers: [$modkey, Shift], key: R, action: Restart),
-        (modifiers: [$modkey], key: J, action: FocusStack, arg: -1),
-        (modifiers: [$modkey], key: K, action: FocusStack, arg: 1),
+        (modifiers: [$modkey], key: H, action: FocusDirection, arg: 2),
+        (modifiers: [$modkey], key: J, action: FocusDirection, arg: 1),
+        (modifiers: [$modkey], key: K, action: FocusDirection, arg: 0),
+        (modifiers: [$modkey], key: L, action: FocusDirection, arg: 3),
         (modifiers: [$modkey], key: Comma, action: FocusMonitor, arg: -1),
         (modifiers: [$modkey], key: Period, action: FocusMonitor, arg: 1),
         (modifiers: [$modkey], key: Key1, action: ViewTag, arg: 0),
@@ -89,17 +91,10 @@
         (modifiers: [$modkey, Shift], key: Key8, action: MoveToTag, arg: 7),
         (modifiers: [$modkey, Shift], key: Key9, action: MoveToTag, arg: 8),
 
-        // Moving Windows
-        (modifiers: [$modkey, Control], key: K, action: SmartMoveWin, arg: 0), // UP
-        (modifiers: [$modkey, Control], key: J, action: SmartMoveWin, arg: 1), // DOWN
-        (modifiers: [$modkey, Control], key: H, action: SmartMoveWin, arg: 2), // LEFT
-        (modifiers: [$modkey, Control], key: L, action: SmartMoveWin, arg: 3), // RIGHT
-
-        // Exchanging Clients
-        (modifiers: [$modkey, Shift], key: K, action: ExchangeClient, arg: 0), // UP
-        (modifiers: [$modkey, Shift], key: J, action: ExchangeClient, arg: 1), // DOWN
-        (modifiers: [$modkey, Shift], key: H, action: ExchangeClient, arg: 2), // LEFT
-        (modifiers: [$modkey, Shift], key: L, action: ExchangeClient, arg: 3), // RIGHT
+        (modifiers: [$modkey, Shift], key: H, action: SwapDirection, arg: 2),
+        (modifiers: [$modkey, Shift], key: J, action: SwapDirection, arg: 1),
+        (modifiers: [$modkey, Shift], key: K, action: SwapDirection, arg: 0),
+        (modifiers: [$modkey, Shift], key: L, action: SwapDirection, arg: 3),
 
         // Example keychord bindings (uncomment to use):
         // KEYCHORDS