oxwm

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

Added window rules such as opening a window in a specific tag, or opening it floating.

Commit
3a6a48d675694b46795330a75ba70a79de260eb8
Parent
fdbc881
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-12-01 06:45:58

Diff

diff --git a/resources/test-config.lua b/resources/test-config.lua
index 4ec331a..10c9357 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -46,6 +46,10 @@ oxwm.gaps.set_smart(true)  -- Disable outer gaps when only 1 window (dwm smartga
 oxwm.gaps.set_inner(5, 5)
 oxwm.gaps.set_outer(5, 5)
 
+oxwm.rule.add({ class = "firefox", title = "Library", floating = true })
+oxwm.rule.add({ instance = "gimp", tag = 5 })
+oxwm.rule.add({ class = "mpv", floating = true })
+
 oxwm.bar.set_font("JetBrainsMono Nerd Font:style=Bold:size=12")
 
 oxwm.bar.set_scheme_normal(colors.fg, colors.bg, 0x444444)
diff --git a/src/config/lua.rs b/src/config/lua.rs
index 5033149..6a16393 100644
--- a/src/config/lua.rs
+++ b/src/config/lua.rs
@@ -42,6 +42,7 @@ pub fn parse_lua_config(
         tags: builder_data.tags,
         layout_symbols: builder_data.layout_symbols,
         keybindings: builder_data.keybindings,
+        window_rules: builder_data.window_rules,
         status_blocks: builder_data.status_blocks,
         scheme_normal: builder_data.scheme_normal,
         scheme_occupied: builder_data.scheme_occupied,
diff --git a/src/config/lua_api.rs b/src/config/lua_api.rs
index 0eff531..b89d6e9 100644
--- a/src/config/lua_api.rs
+++ b/src/config/lua_api.rs
@@ -26,6 +26,7 @@ pub struct ConfigBuilder {
     pub tags: Vec<String>,
     pub layout_symbols: Vec<crate::LayoutSymbolOverride>,
     pub keybindings: Vec<KeyBinding>,
+    pub window_rules: Vec<crate::WindowRule>,
     pub status_blocks: Vec<BlockConfig>,
     pub scheme_normal: ColorScheme,
     pub scheme_occupied: ColorScheme,
@@ -51,6 +52,7 @@ impl Default for ConfigBuilder {
             tags: vec!["1".into(), "2".into(), "3".into()],
             layout_symbols: Vec::new(),
             keybindings: Vec::new(),
+            window_rules: Vec::new(),
             status_blocks: Vec::new(),
             scheme_normal: ColorScheme {
                 foreground: 0xffffff,
@@ -86,6 +88,7 @@ pub fn register_api(lua: &Lua) -> Result<SharedBuilder, ConfigError> {
     register_client_module(&lua, &oxwm_table)?;
     register_layout_module(&lua, &oxwm_table)?;
     register_tag_module(&lua, &oxwm_table)?;
+    register_rule_module(&lua, &oxwm_table, builder.clone())?;
     register_bar_module(&lua, &oxwm_table, builder.clone())?;
     register_misc(&lua, &oxwm_table, builder.clone())?;
 
@@ -321,6 +324,45 @@ fn register_tag_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
     Ok(())
 }
 
+fn register_rule_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
+    let rule_table = lua.create_table()?;
+
+    let builder_clone = builder.clone();
+    let add = lua.create_function(move |_, config: Table| {
+        let class: Option<String> = config.get("class").ok();
+        let instance: Option<String> = config.get("instance").ok();
+        let title: Option<String> = config.get("title").ok();
+        let is_floating: Option<bool> = config.get("floating").ok();
+        let monitor: Option<usize> = config.get("monitor").ok();
+
+        let tags: Option<u32> = if let Ok(tag_index) = config.get::<i32>("tag") {
+            if tag_index > 0 {
+                Some(1 << (tag_index - 1))
+            } else {
+                None
+            }
+        } else {
+            None
+        };
+
+        let rule = crate::WindowRule {
+            class,
+            instance,
+            title,
+            tags,
+            is_floating,
+            monitor,
+        };
+
+        builder_clone.borrow_mut().window_rules.push(rule);
+        Ok(())
+    })?;
+
+    rule_table.set("add", add)?;
+    parent.set("rule", rule_table)?;
+    Ok(())
+}
+
 fn register_bar_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
     let bar_table = lua.create_table()?;
 
diff --git a/src/lib.rs b/src/lib.rs
index d91b97b..1006247 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -12,6 +12,7 @@ pub mod window_manager;
 pub mod prelude {
     pub use crate::ColorScheme;
     pub use crate::LayoutSymbolOverride;
+    pub use crate::WindowRule;
     pub use crate::bar::{BlockCommand, BlockConfig};
     pub use crate::keyboard::{Arg, KeyAction, handlers::KeyBinding, keysyms};
     pub use x11rb::protocol::xproto::KeyButMask;
@@ -23,6 +24,25 @@ pub struct LayoutSymbolOverride {
     pub symbol: String,
 }
 
+#[derive(Clone)]
+pub struct WindowRule {
+    pub class: Option<String>,
+    pub instance: Option<String>,
+    pub title: Option<String>,
+    pub tags: Option<u32>,
+    pub is_floating: Option<bool>,
+    pub monitor: Option<usize>,
+}
+
+impl WindowRule {
+    pub fn matches(&self, class: &str, instance: &str, title: &str) -> bool {
+        let class_matches = self.class.as_ref().map_or(true, |c| class.contains(c.as_str()));
+        let instance_matches = self.instance.as_ref().map_or(true, |i| instance.contains(i.as_str()));
+        let title_matches = self.title.as_ref().map_or(true, |t| title.contains(t.as_str()));
+        class_matches && instance_matches && title_matches
+    }
+}
+
 #[derive(Clone)]
 pub struct Config {
     // Appearance
@@ -52,6 +72,9 @@ pub struct Config {
     // Keybindings
     pub keybindings: Vec<crate::keyboard::handlers::Key>,
 
+    // Window rules
+    pub window_rules: Vec<WindowRule>,
+
     // Status bar
     pub status_blocks: Vec<crate::bar::BlockConfig>,
 
@@ -284,6 +307,7 @@ impl Default for Config {
                     Arg::Int(8),
                 ),
             ],
+            window_rules: vec![],
             status_blocks: vec![crate::bar::BlockConfig {
                 format: "{}".to_string(),
                 command: crate::bar::BlockCommand::DateTime("%a, %b %d - %-I:%M %P".to_string()),
diff --git a/src/window_manager.rs b/src/window_manager.rs
index ab64690..cf0c6f2 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -1824,6 +1824,79 @@ impl WindowManager {
             })
     }
 
+    fn get_window_class_instance(&self, window: Window) -> (String, String) {
+        let reply = self.connection
+            .get_property(false, window, AtomEnum::WM_CLASS, AtomEnum::STRING, 0, 1024)
+            .ok()
+            .and_then(|cookie| cookie.reply().ok());
+
+        if let Some(reply) = reply {
+            if !reply.value.is_empty() {
+                if let Ok(text) = std::str::from_utf8(&reply.value) {
+                    let parts: Vec<&str> = text.split('\0').collect();
+                    let instance = parts.get(0).unwrap_or(&"").to_string();
+                    let class = parts.get(1).unwrap_or(&"").to_string();
+                    return (instance, class);
+                }
+            }
+        }
+
+        (String::new(), String::new())
+    }
+
+    fn apply_rules(&mut self, window: Window) -> WmResult<()> {
+        let (instance, class) = self.get_window_class_instance(window);
+        let title = self.clients.get(&window).map(|c| c.name.clone()).unwrap_or_default();
+
+        let mut rule_tags: Option<u32> = None;
+        let mut rule_floating: Option<bool> = None;
+        let mut rule_monitor: Option<usize> = None;
+
+        for rule in &self.config.window_rules {
+            if rule.matches(&class, &instance, &title) {
+                if rule.tags.is_some() {
+                    rule_tags = rule.tags;
+                }
+                if rule.is_floating.is_some() {
+                    rule_floating = rule.is_floating;
+                }
+                if rule.monitor.is_some() {
+                    rule_monitor = rule.monitor;
+                }
+            }
+        }
+
+        if let Some(client) = self.clients.get_mut(&window) {
+            if let Some(is_floating) = rule_floating {
+                client.is_floating = is_floating;
+                if is_floating {
+                    self.floating_windows.insert(window);
+                } else {
+                    self.floating_windows.remove(&window);
+                }
+            }
+
+            if let Some(monitor_index) = rule_monitor {
+                if monitor_index < self.monitors.len() {
+                    client.monitor_index = monitor_index;
+                }
+            }
+
+            let tags = rule_tags.unwrap_or_else(|| {
+                self.monitors
+                    .get(client.monitor_index)
+                    .map(|m| m.tagset[m.selected_tags_index])
+                    .unwrap_or(tag_mask(0))
+            });
+
+            client.tags = tags;
+            self.window_tags.insert(window, tags);
+            self.window_monitor.insert(window, client.monitor_index);
+        }
+
+        Ok(())
+    }
+
     fn manage_window(&mut self, window: Window) -> WmResult<()> {
         let geometry = self.connection.get_geometry(window)?.reply()?;
         let mut window_x = geometry.x as i32;
@@ -1904,12 +1977,13 @@ impl WindowManager {
         self.clients.insert(window, client);
         self.update_size_hints(window)?;
         self.update_window_title(window)?;
-        self.attach_aside(window, monitor_index);
-        self.attach_stack(window, monitor_index);
+        self.apply_rules(window)?;
+
+        let updated_monitor_index = self.clients.get(&window).map(|c| c.monitor_index).unwrap_or(monitor_index);
+        self.attach_aside(window, updated_monitor_index);
+        self.attach_stack(window, updated_monitor_index);
 
         self.windows.push(window);
-        self.window_tags.insert(window, window_tags);
-        self.window_monitor.insert(window, monitor_index);
 
         if is_transient || is_dialog {
             self.floating_windows.insert(window);