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);