oxwm

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

Updated for commit message requests and added floating (normie layout) and bind to toggle layouts.

Commit
033558201a17bc90a9ea468003e4fb5de375960b
Parent
e4329b5
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-10-25 04:31:08

Diff

diff --git a/Cargo.lock b/Cargo.lock
index 86cca37..7426a71 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -222,7 +222,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
 [[package]]
 name = "oxwm"
-version = "0.2.0"
+version = "0.3.0"
 dependencies = [
  "anyhow",
  "chrono",
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 7389df5..bd1a447 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -19,6 +19,8 @@ pub enum KeyAction {
     ToggleGaps,
     ToggleFullScreen,
     ToggleFloating,
+    ChangeLayout,
+    CycleLayout,
     MoveToTag,
     FocusMonitor,
     None,
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index f8a9918..ab09ff8 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,3 +1,4 @@
+pub mod normie;
 pub mod tiling;
 
 use x11rb::protocol::xproto::Window;
@@ -9,6 +10,26 @@ pub struct GapConfig {
     pub outer_vertical: u32,
 }
 
+pub const TILING: &str = "tiling";
+pub const NORMIE: &str = "normie";
+pub const FLOATING: &str = "floating";
+
+pub fn layout_from_str(s: &str) -> Result<Box<dyn Layout>, String> {
+    match s.to_lowercase().as_str() {
+        TILING => Ok(Box::new(tiling::TilingLayout)),
+        NORMIE | FLOATING => Ok(Box::new(normie::NormieLayout)),
+        _ => Err(format!("Unknown layout: {}", s)),
+    }
+}
+
+pub fn next_layout(current_name: &str) -> &'static str {
+    match current_name {
+        TILING => NORMIE,
+        NORMIE => TILING,
+        _ => TILING,
+    }
+}
+
 pub trait Layout {
     fn arrange(
         &self,
@@ -17,6 +38,7 @@ pub trait Layout {
         screen_height: u32,
         gaps: &GapConfig,
     ) -> Vec<WindowGeometry>;
+    fn name(&self) -> &'static str;
 }
 
 pub struct WindowGeometry {
diff --git a/src/layout/normie.rs b/src/layout/normie.rs
new file mode 100644
index 0000000..a7fd756
--- /dev/null
+++ b/src/layout/normie.rs
@@ -0,0 +1,43 @@
+use super::{GapConfig, Layout, WindowGeometry};
+use x11rb::protocol::xproto::Window;
+
+pub struct NormieLayout;
+
+impl Layout for NormieLayout {
+    fn name(&self) -> &'static str {
+        super::NORMIE
+    }
+
+    fn arrange(
+        &self,
+        windows: &[Window],
+        screen_width: u32,
+        screen_height: u32,
+        _gaps: &GapConfig,
+    ) -> Vec<WindowGeometry> {
+        // Floating layout: all windows open centered with a reasonable default size
+        // This mimics dwm's NULL layout behavior where windows float freely
+        const DEFAULT_WIDTH_RATIO: f32 = 0.6;
+        const DEFAULT_HEIGHT_RATIO: f32 = 0.6;
+
+        windows
+            .iter()
+            .map(|_| {
+                // Calculate default window dimensions (60% of screen)
+                let width = ((screen_width as f32) * DEFAULT_WIDTH_RATIO) as u32;
+                let height = ((screen_height as f32) * DEFAULT_HEIGHT_RATIO) as u32;
+
+                // Center the window on the screen
+                let x = ((screen_width - width) / 2) as i32;
+                let y = ((screen_height - height) / 2) as i32;
+
+                WindowGeometry {
+                    x_coordinate: x,
+                    y_coordinate: y,
+                    width,
+                    height,
+                }
+            })
+            .collect()
+    }
+}
diff --git a/src/layout/tiling.rs b/src/layout/tiling.rs
index 6bfef1f..a31589e 100644
--- a/src/layout/tiling.rs
+++ b/src/layout/tiling.rs
@@ -4,6 +4,10 @@ use x11rb::protocol::xproto::Window;
 pub struct TilingLayout;
 
 impl Layout for TilingLayout {
+    fn name(&self) -> &'static str {
+        super::TILING
+    }
+
     fn arrange(
         &self,
         windows: &[Window],
diff --git a/src/lib.rs b/src/lib.rs
index e4dc293..d5086de 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -99,6 +99,7 @@ impl Default for Config {
                     ]),
                 ),
                 Key::new(vec![MODKEY], keycodes::Q, KeyAction::KillClient, Arg::None),
+                Key::new(vec![MODKEY], keycodes::N, KeyAction::CycleLayout, Arg::None),
                 Key::new(
                     vec![MODKEY, SHIFT],
                     keycodes::F,
diff --git a/src/monitor.rs b/src/monitor.rs
index 05602aa..b7de736 100644
--- a/src/monitor.rs
+++ b/src/monitor.rs
@@ -40,56 +40,70 @@ pub fn detect_monitors(
     screen: &Screen,
     _root: Window,
 ) -> WmResult<Vec<Monitor>> {
-    let mut monitors = Vec::new();
-
-    if let Ok(cookie) = connection.xinerama_is_active() {
-        if let Ok(reply) = cookie.reply() {
-            if reply.state != 0 {
-                if let Ok(screens_cookie) = connection.xinerama_query_screens() {
-                    if let Ok(screens_reply) = screens_cookie.reply() {
-                        for screen_info in &screens_reply.screen_info {
-                            if screen_info.width == 0 || screen_info.height == 0 {
-                                continue;
-                            }
-
-                            let is_unique = !monitors.iter().any(|m: &Monitor| {
-                                m.x == screen_info.x_org as i32
-                                    && m.y == screen_info.y_org as i32
-                                    && m.width == screen_info.width as u32
-                                    && m.height == screen_info.height as u32
-                            });
-
-                            if is_unique {
-                                monitors.push(Monitor::new(
-                                    screen_info.x_org as i32,
-                                    screen_info.y_org as i32,
-                                    screen_info.width as u32,
-                                    screen_info.height as u32,
-                                ));
-                            }
-                        }
-                    }
-                }
+    let fallback_monitors = || {
+        vec![Monitor::new(
+            0,
+            0,
+            screen.width_in_pixels as u32,
+            screen.height_in_pixels as u32,
+        )]
+    };
+
+    let mut monitors = Vec::<Monitor>::new();
+
+    let xinerama_active = connection
+        .xinerama_is_active()
+        .ok()
+        .and_then(|cookie| cookie.reply().ok())
+        .map_or(false, |reply| reply.state != 0);
+
+    if xinerama_active {
+        let xinerama_cookie = match connection.xinerama_query_screens() {
+            Ok(cookie) => cookie,
+            Err(_) => return Ok(fallback_monitors()),
+        };
+
+        let xinerama_reply = match xinerama_cookie.reply() {
+            Ok(reply) => reply,
+            Err(_) => return Ok(fallback_monitors()),
+        };
+
+        for screen_info in &xinerama_reply.screen_info {
+            let has_valid_dimensions = screen_info.width > 0 && screen_info.height > 0;
+            if !has_valid_dimensions {
+                continue;
+            }
+
+            let x_position = screen_info.x_org as i32;
+            let y_position = screen_info.y_org as i32;
+            let width_in_pixels = screen_info.width as u32;
+            let height_in_pixels = screen_info.height as u32;
+
+            let is_duplicate_monitor = monitors.iter().any(|monitor| {
+                monitor.x == x_position
+                    && monitor.y == y_position
+                    && monitor.width == width_in_pixels
+                    && monitor.height == height_in_pixels
+            });
+
+            if !is_duplicate_monitor {
+                monitors.push(Monitor::new(
+                    x_position,
+                    y_position,
+                    width_in_pixels,
+                    height_in_pixels,
+                ));
             }
         }
     }
 
     if monitors.is_empty() {
-        monitors.push(Monitor::new(
-            0,
-            0,
-            screen.width_in_pixels as u32,
-            screen.height_in_pixels as u32,
-        ));
+        monitors = fallback_monitors();
     }
 
-    monitors.sort_by(|a, b| {
-        let y_cmp = a.y.cmp(&b.y);
-        if y_cmp == std::cmp::Ordering::Equal {
-            a.x.cmp(&b.x)
-        } else {
-            y_cmp
-        }
+    monitors.sort_by(|a, b| match a.y.cmp(&b.y) {
+        std::cmp::Ordering::Equal => a.x.cmp(&b.x),
+        other => other,
     });
 
     Ok(monitors)
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 3e96ccb..10f0bf3 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -3,8 +3,8 @@ use crate::bar::Bar;
 use crate::errors::WmError;
 use crate::keyboard::{self, Arg, KeyAction, handlers};
 use crate::layout::GapConfig;
-use crate::layout::Layout;
 use crate::layout::tiling::TilingLayout;
+use crate::layout::{Layout, layout_from_str, next_layout};
 use crate::monitor::{Monitor, detect_monitors};
 use std::collections::HashSet;
 use x11rb::cursor::Handle as CursorHandle;
@@ -37,10 +37,7 @@ impl AtomCache {
             .reply()?
             .atom;
 
-        let wm_state = connection
-            .intern_atom(false, b"WM_STATE")?
-            .reply()?
-            .atom;
+        let wm_state = connection.intern_atom(false, b"WM_STATE")?.reply()?.atom;
 
         Ok(Self {
             net_current_desktop,
@@ -128,12 +125,7 @@ impl WindowManager {
             u16::from(config.modkey).into(),
         )?;
 
-        let mut monitors = detect_monitors(&connection, &screen, root)?;
-
-        let selected_tags = Self::get_saved_selected_tags(&connection, root, config.tags.len())?;
-        if !monitors.is_empty() {
-            monitors[0].selected_tags = selected_tags;
-        }
+        let monitors = detect_monitors(&connection, &screen, root)?;
 
         let display = unsafe { x11::xlib::XOpenDisplay(std::ptr::null()) };
         if display.is_null() {
@@ -308,7 +300,11 @@ impl WindowManager {
             }
         }
 
-        Ok(self.monitors.get(self.selected_monitor).map(|m| m.selected_tags).unwrap_or(tag_mask(0)))
+        Ok(self
+            .monitors
+            .get(self.selected_monitor)
+            .map(|m| m.selected_tags)
+            .unwrap_or(tag_mask(0)))
     }
 
     fn save_client_tag(&self, window: Window, tag: TagMask) -> WmResult<()> {
@@ -376,7 +372,11 @@ impl WindowManager {
     }
 
     fn toggle_floating(&mut self) -> WmResult<()> {
-        if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+        if let Some(focused) = self
+            .monitors
+            .get(self.selected_monitor)
+            .and_then(|m| m.focused_window)
+        {
             if self.floating_windows.contains(&focused) {
                 self.floating_windows.remove(&focused);
                 self.apply_layout()?;
@@ -385,9 +385,10 @@ impl WindowManager {
                 let float_height = (self.screen.height_in_pixels / 2) as u32;
 
                 let border_width = self.config.border_width;
-                
-                let center_width = ((self.screen.width_in_pixels - float_width as u16)/2) as i32;
-                let center_height = ((self.screen.height_in_pixels - float_height as u16)/2) as i32;
+
+                let center_width = ((self.screen.width_in_pixels - float_width as u16) / 2) as i32;
+                let center_height =
+                    ((self.screen.height_in_pixels - float_height as u16) / 2) as i32;
 
                 self.connection.configure_window(
                     focused,
@@ -409,7 +410,11 @@ impl WindowManager {
     }
 
     fn toggle_fullscreen(&mut self) -> WmResult<()> {
-        if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+        if let Some(focused) = self
+            .monitors
+            .get(self.selected_monitor)
+            .and_then(|m| m.focused_window)
+        {
             if self.fullscreen_window == Some(focused) {
                 self.fullscreen_window = None;
 
@@ -458,7 +463,14 @@ impl WindowManager {
 
                 let draw_blocks = monitor_index == self.selected_monitor;
                 bar.invalidate();
-                bar.draw(&self.connection, &self.font, self.display, monitor.selected_tags, occupied_tags, draw_blocks)?;
+                bar.draw(
+                    &self.connection,
+                    &self.font,
+                    self.display,
+                    monitor.selected_tags,
+                    occupied_tags,
+                    draw_blocks,
+                )?;
             }
         }
         Ok(())
@@ -468,7 +480,11 @@ impl WindowManager {
         match action {
             KeyAction::Spawn => handlers::handle_spawn_action(action, arg)?,
             KeyAction::KillClient => {
-                if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+                if let Some(focused) = self
+                    .monitors
+                    .get(self.selected_monitor)
+                    .and_then(|m| m.focused_window)
+                {
                     match self.connection.kill_client(focused) {
                         Ok(_) => {
                             self.connection.flush()?;
@@ -482,6 +498,28 @@ impl WindowManager {
             KeyAction::ToggleFullScreen => {
                 self.toggle_fullscreen()?;
             }
+            KeyAction::ChangeLayout => {
+                if let Arg::Str(layout_name) = arg {
+                    match layout_from_str(layout_name) {
+                        Ok(layout) => {
+                            self.layout = layout;
+                            self.apply_layout()?;
+                        }
+                        Err(e) => eprintln!("Failed to change layout: {}", e),
+                    }
+                }
+            }
+            KeyAction::CycleLayout => {
+                let current_name = self.layout.name();
+                let next_name = next_layout(current_name);
+                match layout_from_str(next_name) {
+                    Ok(layout) => {
+                        self.layout = layout;
+                        self.apply_layout()?;
+                    }
+                    Err(e) => eprintln!("Failed to cycle layout: {}", e),
+                }
+            }
             KeyAction::ToggleFloating => {
                 self.toggle_floating()?;
             }
@@ -645,7 +683,11 @@ impl WindowManager {
     fn save_selected_tags(&self) -> WmResult<()> {
         let net_current_desktop = self.atoms.net_current_desktop;
 
-        let selected_tags = self.monitors.get(self.selected_monitor).map(|m| m.selected_tags).unwrap_or(tag_mask(0));
+        let selected_tags = self
+            .monitors
+            .get(self.selected_monitor)
+            .map(|m| m.selected_tags)
+            .unwrap_or(tag_mask(0));
         let desktop = selected_tags.trailing_zeros();
 
         let bytes = (desktop as u32).to_ne_bytes();
@@ -668,7 +710,11 @@ impl WindowManager {
             return Ok(());
         }
 
-        if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+        if let Some(focused) = self
+            .monitors
+            .get(self.selected_monitor)
+            .and_then(|m| m.focused_window)
+        {
             let mask = tag_mask(tag_index);
             self.window_tags.insert(focused, mask);
 
@@ -689,7 +735,10 @@ impl WindowManager {
             return Ok(());
         }
 
-        let current = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window);
+        let current = self
+            .monitors
+            .get(self.selected_monitor)
+            .and_then(|m| m.focused_window);
 
         let next_window = if let Some(current) = current {
             if let Some(current_index) = visible.iter().position(|&w| w == current) {
@@ -726,7 +775,11 @@ impl WindowManager {
         Ok(())
     }
 
-    fn update_focus_visuals(&self, old_focused: Option<Window>, new_focused: Window) -> WmResult<()> {
+    fn update_focus_visuals(
+        &self,
+        old_focused: Option<Window>,
+        new_focused: Window,
+    ) -> WmResult<()> {
         if let Some(old_win) = old_focused {
             if old_win != new_focused {
                 self.connection.configure_window(
@@ -894,11 +947,16 @@ impl WindowManager {
                     &ChangeWindowAttributesAux::new().event_mask(EventMask::ENTER_WINDOW),
                 )?;
 
-                let selected_tags = self.monitors.get(self.selected_monitor).map(|m| m.selected_tags).unwrap_or(tag_mask(0));
+                let selected_tags = self
+                    .monitors
+                    .get(self.selected_monitor)
+                    .map(|m| m.selected_tags)
+                    .unwrap_or(tag_mask(0));
 
                 self.windows.push(event.window);
                 self.window_tags.insert(event.window, selected_tags);
-                self.window_monitor.insert(event.window, self.selected_monitor);
+                self.window_monitor
+                    .insert(event.window, self.selected_monitor);
                 self.set_wm_state(event.window, 1)?;
                 let _ = self.save_client_tag(event.window, selected_tags);
 
@@ -929,7 +987,9 @@ impl WindowManager {
                     return Ok(None);
                 }
 
-                if let Some(monitor_index) = self.get_monitor_at_point(event.root_x as i32, event.root_y as i32) {
+                if let Some(monitor_index) =
+                    self.get_monitor_at_point(event.root_x as i32, event.root_y as i32)
+                {
                     if monitor_index != self.selected_monitor {
                         self.selected_monitor = monitor_index;
                         self.update_bar()?;
@@ -950,7 +1010,9 @@ impl WindowManager {
                 }
             }
             Event::ButtonPress(event) => {
-                let is_bar_click = self.bars.iter()
+                let is_bar_click = self
+                    .bars
+                    .iter()
                     .enumerate()
                     .find(|(_, bar)| bar.window() == event.event);
 
@@ -1029,7 +1091,11 @@ impl WindowManager {
                 .copied()
                 .collect();
 
-            let bar_height = self.bars.get(monitor_index).map(|b| b.height() as u32).unwrap_or(0);
+            let bar_height = self
+                .bars
+                .get(monitor_index)
+                .map(|b| b.height() as u32)
+                .unwrap_or(0);
             let usable_height = monitor.height.saturating_sub(bar_height);
 
             let geometries = self
@@ -1058,6 +1124,12 @@ impl WindowManager {
         Ok(())
     }
 
+    pub fn change_layout<L: Layout + 'static>(&mut self, new_layout: L) -> WmResult<()> {
+        self.layout = Box::new(new_layout);
+        self.apply_layout()?;
+        Ok(())
+    }
+
     fn remove_window(&mut self, window: Window) -> WmResult<()> {
         let initial_count = self.windows.len();
         self.windows.retain(|&w| w != window);
@@ -1073,7 +1145,10 @@ impl WindowManager {
         }
 
         if self.windows.len() < initial_count {
-            let focused = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window);
+            let focused = self
+                .monitors
+                .get(self.selected_monitor)
+                .and_then(|m| m.focused_window);
             if focused == Some(window) {
                 let visible = self.visible_windows_on_monitor(self.selected_monitor);
                 if let Some(&new_win) = visible.last() {
diff --git a/templates/config.ron b/templates/config.ron
index 3a62643..18dcd89 100644
--- a/templates/config.ron
+++ b/templates/config.ron
@@ -26,6 +26,8 @@
         (modifiers: [Mod4], key: Q, action: KillClient),
         (modifiers: [Mod4, Shift], key: F, action: ToggleFullScreen),
         (modifiers: [Mod4, Shift], key: Space, action: ToggleFloating),
+        (modifiers: [Mod4], key: F, action: ChangeLayout, arg: "normie"),
+        (modifiers: [Mod4], key: C, action: ChangeLayout, arg: "tiling"),
         (modifiers: [Mod4], key: A, action: ToggleGaps),
         (modifiers: [Mod4, Shift], key: Q, action: Quit),
         (modifiers: [Mod4, Shift], key: R, action: Restart),
diff --git a/test-config.ron b/test-config.ron
index 5425704..e264036 100644
--- a/test-config.ron
+++ b/test-config.ron
@@ -32,6 +32,9 @@
         (modifiers: [Mod1], key: Q, action: KillClient),
         (modifiers: [Mod1, Shift], key: F, action: ToggleFullScreen),
         (modifiers: [Mod1, Shift], key: Space, action: ToggleFloating),
+        (modifiers: [Mod1], key: F, action: ChangeLayout, arg: "normie"),
+        (modifiers: [Mod1], key: C, action: ChangeLayout, arg: "tiling"),
+        (modifiers: [Mod1], key: N, action: CycleLayout),
         (modifiers: [Mod1], key: A, action: ToggleGaps),
         (modifiers: [Mod1, Shift], key: Q, action: Quit),
         (modifiers: [Mod1, Shift], key: R, action: Restart),