oxwm

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

Added smartgaps, mfact, and nmaster (inc master, dec master) functionality from dwm. Default to attach-aside patch.

Commit
16b90b9ce0f36ef5bfe41dc4f77630c325b681d3
Parent
b045fc8
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-12-01 05:12:48

Diff

diff --git a/resources/test-config.lua b/resources/test-config.lua
index ca9013f..1f8094f 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -72,6 +72,14 @@ oxwm.key.bind({ modkey }, "N", oxwm.layout.cycle())
 
 oxwm.key.bind({ modkey }, "A", oxwm.toggle_gaps())
 
+-- Master area controls (dwm-style)
+-- NOTE: Mod+H/L are currently used for directional focus
+-- Using Comma/Period instead. To match dwm exactly, replace H/L bindings above
+oxwm.key.bind({ modkey }, "Comma", oxwm.set_master_factor(-5))   -- Decrease master area
+oxwm.key.bind({ modkey }, "Period", oxwm.set_master_factor(5))    -- Increase master area
+oxwm.key.bind({ modkey }, "I", oxwm.inc_num_master(1))             -- More master windows
+oxwm.key.bind({ modkey }, "P", oxwm.inc_num_master(-1))            -- Fewer master windows
+
 oxwm.key.bind({ modkey, "Shift" }, "Q", oxwm.quit())
 oxwm.key.bind({ modkey, "Shift" }, "R", oxwm.restart())
 
diff --git a/src/config/lua_api.rs b/src/config/lua_api.rs
index 5057830..4dc33b9 100644
--- a/src/config/lua_api.rs
+++ b/src/config/lua_api.rs
@@ -588,6 +588,14 @@ fn register_misc(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<()
         create_action_table(lua, "ToggleGaps", Value::Nil)
     })?;
 
+    let set_master_factor = lua.create_function(|lua, delta: i32| {
+        create_action_table(lua, "SetMasterFactor", Value::Integer(delta as i64))
+    })?;
+
+    let inc_num_master = lua.create_function(|lua, delta: i32| {
+        create_action_table(lua, "IncNumMaster", Value::Integer(delta as i64))
+    })?;
+
     let show_keybinds = lua.create_function(|lua, ()| {
         create_action_table(lua, "ShowKeybindOverlay", Value::Nil)
     })?;
@@ -620,6 +628,8 @@ fn register_misc(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<()
     parent.set("restart", restart)?;
     parent.set("recompile", recompile)?;
     parent.set("toggle_gaps", toggle_gaps)?;
+    parent.set("set_master_factor", set_master_factor)?;
+    parent.set("inc_num_master", inc_num_master)?;
     parent.set("show_keybinds", show_keybinds)?;
     parent.set("focus_monitor", focus_monitor)?;
     Ok(())
@@ -708,6 +718,8 @@ fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
         "Recompile" => Ok(KeyAction::Recompile),
         "ViewTag" => Ok(KeyAction::ViewTag),
         "ToggleGaps" => Ok(KeyAction::ToggleGaps),
+        "SetMasterFactor" => Ok(KeyAction::SetMasterFactor),
+        "IncNumMaster" => Ok(KeyAction::IncNumMaster),
         "ToggleFullScreen" => Ok(KeyAction::ToggleFullScreen),
         "ToggleFloating" => Ok(KeyAction::ToggleFloating),
         "ChangeLayout" => Ok(KeyAction::ChangeLayout),
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 79a44cd..475ecbd 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -30,6 +30,8 @@ pub enum KeyAction {
     FocusMonitor,
     ExchangeClient,
     ShowKeybindOverlay,
+    SetMasterFactor,
+    IncNumMaster,
     None,
 }
 
diff --git a/src/layout/grid.rs b/src/layout/grid.rs
index a8962ec..7ad68e3 100644
--- a/src/layout/grid.rs
+++ b/src/layout/grid.rs
@@ -18,6 +18,8 @@ impl Layout for GridLayout {
         screen_width: u32,
         screen_height: u32,
         gaps: &GapConfig,
+        _master_factor: f32,
+        _num_master: i32,
     ) -> Vec<WindowGeometry> {
         let window_count = windows.len();
         if window_count == 0 {
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 6e92db1..61db85e 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -86,6 +86,8 @@ pub trait Layout {
         screen_width: u32,
         screen_height: u32,
         gaps: &GapConfig,
+        master_factor: f32,
+        num_master: i32,
     ) -> Vec<WindowGeometry>;
     fn name(&self) -> &'static str;
     fn symbol(&self) -> &'static str;
diff --git a/src/layout/monocle.rs b/src/layout/monocle.rs
index 8bc8d10..e97b221 100644
--- a/src/layout/monocle.rs
+++ b/src/layout/monocle.rs
@@ -18,6 +18,8 @@ impl Layout for MonocleLayout {
         screen_width: u32,
         screen_height: u32,
         gaps: &GapConfig,
+        _master_factor: f32,
+        _num_master: i32,
     ) -> Vec<WindowGeometry> {
         let window_count = windows.len();
         if window_count == 0 {
diff --git a/src/layout/normie.rs b/src/layout/normie.rs
index bc12b0e..2e1d4a3 100644
--- a/src/layout/normie.rs
+++ b/src/layout/normie.rs
@@ -19,6 +19,8 @@ impl Layout for NormieLayout {
         screen_width: u32,
         screen_height: u32,
         _gaps: &GapConfig,
+        _master_factor: f32,
+        _num_master: i32,
     ) -> Vec<WindowGeometry> {
         const DEFAULT_WIDTH_RATIO: f32 = 0.6;
         const DEFAULT_HEIGHT_RATIO: f32 = 0.6;
diff --git a/src/layout/tabbed.rs b/src/layout/tabbed.rs
index 9245c1b..ee9f75e 100644
--- a/src/layout/tabbed.rs
+++ b/src/layout/tabbed.rs
@@ -20,6 +20,8 @@ impl Layout for TabbedLayout {
         screen_width: u32,
         screen_height: u32,
         gaps: &GapConfig,
+        _master_factor: f32,
+        _num_master: i32,
     ) -> Vec<WindowGeometry> {
         let window_count = windows.len();
         if window_count == 0 {
diff --git a/src/layout/tiling.rs b/src/layout/tiling.rs
index dc0e1bf..a67defb 100644
--- a/src/layout/tiling.rs
+++ b/src/layout/tiling.rs
@@ -3,6 +3,73 @@ use x11rb::protocol::xproto::Window;
 
 pub struct TilingLayout;
 
+struct GapValues {
+    outer_horizontal: u32,
+    outer_vertical: u32,
+    inner_horizontal: u32,
+    inner_vertical: u32,
+}
+
+struct FactValues {
+    master_facts: f32,
+    stack_facts: f32,
+    master_remainder: i32,
+    stack_remainder: i32,
+}
+
+impl TilingLayout {
+    fn getgaps(gaps: &GapConfig, window_count: usize, smartgaps_enabled: bool) -> GapValues {
+        let outer_enabled = if smartgaps_enabled && window_count == 1 {
+            0
+        } else {
+            1
+        };
+        let inner_enabled = 1;
+
+        GapValues {
+            outer_horizontal: gaps.outer_horizontal * outer_enabled,
+            outer_vertical: gaps.outer_vertical * outer_enabled,
+            inner_horizontal: gaps.inner_horizontal * inner_enabled,
+            inner_vertical: gaps.inner_vertical * inner_enabled,
+        }
+    }
+
+    fn getfacts(
+        window_count: usize,
+        num_master: i32,
+        master_size: i32,
+        stack_size: i32,
+    ) -> FactValues {
+        let num_master = num_master.max(0) as usize;
+        let master_facts = window_count.min(num_master) as f32;
+        let stack_facts = if window_count > num_master {
+            (window_count - num_master) as f32
+        } else {
+            0.0
+        };
+
+        let mut master_total = 0;
+        let mut stack_total = 0;
+
+        for i in 0..window_count {
+            if i < num_master {
+                master_total += (master_size as f32 / master_facts) as i32;
+            } else {
+                if stack_facts > 0.0 {
+                    stack_total += (stack_size as f32 / stack_facts) as i32;
+                }
+            }
+        }
+
+        FactValues {
+            master_facts,
+            stack_facts,
+            master_remainder: master_size - master_total,
+            stack_remainder: stack_size - stack_total,
+        }
+    }
+}
+
 impl Layout for TilingLayout {
     fn name(&self) -> &'static str {
         super::LayoutType::Tiling.as_str()
@@ -18,68 +85,94 @@ impl Layout for TilingLayout {
         screen_width: u32,
         screen_height: u32,
         gaps: &GapConfig,
+        master_factor: f32,
+        num_master: i32,
     ) -> Vec<WindowGeometry> {
         let window_count = windows.len();
         if window_count == 0 {
             return Vec::new();
         }
 
-        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);
-
-            vec![WindowGeometry {
-                x_coordinate: x,
-                y_coordinate: y,
-                width,
-                height,
-            }]
+        let smartgaps_enabled = true;
+        let gap_values = Self::getgaps(gaps, window_count, smartgaps_enabled);
+
+        let outer_gap_horizontal = gap_values.outer_horizontal;
+        let outer_gap_vertical = gap_values.outer_vertical;
+        let inner_gap_horizontal = gap_values.inner_horizontal;
+        let inner_gap_vertical = gap_values.inner_vertical;
+
+        let mut stack_x = outer_gap_vertical as i32;
+        let mut stack_y = outer_gap_horizontal as i32;
+        let master_x = outer_gap_vertical as i32;
+        let mut master_y = outer_gap_horizontal as i32;
+
+        let num_master_usize = num_master.max(0) as usize;
+        let master_count = window_count.min(num_master_usize);
+        let stack_count = if window_count > num_master_usize {
+            window_count - num_master_usize
         } else {
-            let mut geometries = Vec::new();
+            0
+        };
 
-            let master_width = (screen_width / 2)
-                .saturating_sub(gaps.outer_horizontal)
-                .saturating_sub(gaps.inner_horizontal / 2);
+        let master_height = (screen_height as i32)
+            - (2 * outer_gap_horizontal) as i32
+            - (inner_gap_horizontal as i32 * (master_count.saturating_sub(1)) as i32);
+        let stack_height = (screen_height as i32)
+            - (2 * outer_gap_horizontal) as i32
+            - (inner_gap_horizontal as i32 * stack_count.saturating_sub(1) as i32);
+        let mut stack_width = (screen_width as i32) - (2 * outer_gap_vertical) as i32;
+        let mut master_width = stack_width;
 
-            let master_x = gaps.outer_horizontal as i32;
-            let master_y = gaps.outer_vertical as i32;
-            let master_height = screen_height.saturating_sub(2 * gaps.outer_vertical);
+        if num_master > 0 && window_count > num_master_usize {
+            stack_width = ((master_width as f32 - inner_gap_vertical as f32) * (1.0 - master_factor)) as i32;
+            master_width = master_width - inner_gap_vertical as i32 - stack_width;
+            stack_x = master_x + master_width + inner_gap_vertical as i32;
+        }
 
-            geometries.push(WindowGeometry {
-                x_coordinate: master_x,
-                y_coordinate: master_y,
-                width: master_width,
-                height: master_height,
-            });
+        let facts = Self::getfacts(window_count, num_master, master_height, stack_height);
 
-            let stack_count = window_count - 1;
-            let stack_x = (screen_width / 2 + gaps.inner_horizontal / 2) as i32;
-            let stack_width = (screen_width / 2)
-                .saturating_sub(gaps.outer_horizontal)
-                .saturating_sub(gaps.inner_horizontal / 2);
+        let mut geometries = Vec::new();
 
-            let total_stack_height = screen_height.saturating_sub(2 * gaps.outer_vertical);
+        for (i, _window) in windows.iter().enumerate() {
+            if i < num_master_usize {
+                let window_height = (master_height as f32 / facts.master_facts) as i32
+                    + if (i as i32) < facts.master_remainder {
+                        1
+                    } else {
+                        0
+                    };
 
-            let total_inner_gaps = gaps.inner_vertical * (stack_count as u32 - 1);
-            let stack_height =
-                total_stack_height.saturating_sub(total_inner_gaps) / stack_count as u32;
+                geometries.push(WindowGeometry {
+                    x_coordinate: master_x,
+                    y_coordinate: master_y,
+                    width: master_width as u32,
+                    height: window_height as u32,
+                });
 
-            for i in 1..window_count {
-                let stack_index = i - 1;
-                let y_offset = gaps.outer_vertical
-                    + (stack_index as u32) * (stack_height + gaps.inner_vertical);
+                master_y += window_height + inner_gap_horizontal as i32;
+            } else {
+                let window_height = if facts.stack_facts > 0.0 {
+                    (stack_height as f32 / facts.stack_facts) as i32
+                        + if ((i - num_master_usize) as i32) < facts.stack_remainder {
+                            1
+                        } else {
+                            0
+                        }
+                } else {
+                    stack_height
+                };
 
                 geometries.push(WindowGeometry {
                     x_coordinate: stack_x,
-                    y_coordinate: y_offset as i32,
-                    width: stack_width,
-                    height: stack_height,
+                    y_coordinate: stack_y,
+                    width: stack_width as u32,
+                    height: window_height as u32,
                 });
-            }
 
-            return geometries;
+                stack_y += window_height + inner_gap_horizontal as i32;
+            }
         }
+
+        geometries
     }
 }
diff --git a/src/overlay/keybind.rs b/src/overlay/keybind.rs
index a530eae..3a13bfe 100644
--- a/src/overlay/keybind.rs
+++ b/src/overlay/keybind.rs
@@ -217,6 +217,8 @@ impl KeybindOverlay {
             KeyAction::CycleLayout => "Cycle Through Layouts".to_string(),
             KeyAction::FocusMonitor => "Focus Next Monitor".to_string(),
             KeyAction::ExchangeClient => "Exchange Client Windows".to_string(),
+            KeyAction::SetMasterFactor => "Adjust Master Area Size".to_string(),
+            KeyAction::IncNumMaster => "Adjust Number of Master Windows".to_string(),
             KeyAction::None => "No Action".to_string(),
         }
     }
diff --git a/src/window_manager.rs b/src/window_manager.rs
index e22d214..97bc9e5 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -594,6 +594,24 @@ impl WindowManager {
         Ok(())
     }
 
+    fn set_master_factor(&mut self, delta: f32) -> WmResult<()> {
+        if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
+            let new_mfact = (monitor.master_factor + delta).max(0.05).min(0.95);
+            monitor.master_factor = new_mfact;
+            self.apply_layout()?;
+        }
+        Ok(())
+    }
+
+    fn inc_num_master(&mut self, delta: i32) -> WmResult<()> {
+        if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
+            let new_nmaster = (monitor.num_master + delta).max(0);
+            monitor.num_master = new_nmaster;
+            self.apply_layout()?;
+        }
+        Ok(())
+    }
+
     fn exchange_client(&mut self, direction: i32) -> WmResult<()> {
         let focused = match self
             .monitors
@@ -907,6 +925,16 @@ impl WindowManager {
                     monitor.screen_height as u16,
                 )?;
             }
+            KeyAction::SetMasterFactor => {
+                if let Arg::Int(delta) = arg {
+                    self.set_master_factor(*delta as f32 / 100.0)?;
+                }
+            }
+            KeyAction::IncNumMaster => {
+                if let Arg::Int(delta) = arg {
+                    self.inc_num_master(*delta)?;
+                }
+            }
             KeyAction::None => {}
         }
         Ok(())
@@ -1732,7 +1760,7 @@ impl WindowManager {
         self.clients.insert(window, client);
         self.update_size_hints(window)?;
         self.update_window_title(window)?;
-        self.attach(window, monitor_index);
+        self.attach_aside(window, monitor_index);
         self.attach_stack(window, monitor_index);
 
         self.windows.push(window);
@@ -2668,10 +2696,17 @@ impl WindowManager {
                 0
             };
             let usable_height = monitor_height.saturating_sub(bar_height as i32);
-
-            let geometries = self
-                .layout
-                .arrange(&visible, monitor_width as u32, usable_height as u32, &gaps);
+            let master_factor = monitor.master_factor;
+            let num_master = monitor.num_master;
+
+            let geometries = self.layout.arrange(
+                &visible,
+                monitor_width as u32,
+                usable_height as u32,
+                &gaps,
+                master_factor,
+                num_master,
+            );
 
             for (window, geometry) in visible.iter().zip(geometries.iter()) {
                 let adjusted_width = geometry.width.saturating_sub(2 * border_width);
@@ -3044,6 +3079,49 @@ impl WindowManager {
         }
     }
 
+    fn attach_aside(&mut self, window: Window, monitor_index: usize) {
+        let monitor = match self.monitors.get(monitor_index) {
+            Some(m) => m,
+            None => return,
+        };
+
+        if monitor.clients_head.is_none() {
+            self.attach(window, monitor_index);
+            return;
+        }
+
+        let num_master = monitor.num_master.max(1) as usize;
+        let mut current = monitor.clients_head;
+        let mut position = 0;
+
+        while position < num_master - 1 {
+            if let Some(current_window) = current {
+                if let Some(current_client) = self.clients.get(&current_window) {
+                    current = current_client.next;
+                    position += 1;
+                } else {
+                    break;
+                }
+            } else {
+                break;
+            }
+        }
+
+        if let Some(insert_after) = current {
+            if let Some(after_client) = self.clients.get(&insert_after) {
+                let old_next = after_client.next;
+                if let Some(new_client) = self.clients.get_mut(&window) {
+                    new_client.next = old_next;
+                }
+                if let Some(after_client_mut) = self.clients.get_mut(&insert_after) {
+                    after_client_mut.next = Some(window);
+                }
+            }
+        } else {
+            self.attach(window, monitor_index);
+        }
+    }
+
     fn detach(&mut self, window: Window) {
         let monitor_index = self.clients.get(&window).map(|c| c.monitor_index);
         if let Some(monitor_index) = monitor_index {