From d76cb13a1f0b43259e3f9f0479932a9aa1305fbd Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 7 Sep 2023 20:36:49 -0500 Subject: [PATCH] Simplify window rules API --- api/lua/example_config.lua | 24 ++++- api/lua/tag.lua | 166 ++++++++++++--------------------- api/lua/window.lua | 12 +-- api/lua/window_rules.lua | 155 +++++++++++++++++++++++++----- api/lua/window_rules_types.lua | 18 ++-- src/api/msg/window_rules.rs | 126 ++++++++++++++++++++----- src/handlers/xwayland.rs | 7 -- 7 files changed, 333 insertions(+), 175 deletions(-) diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index 40149eb..74167cc 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -90,7 +90,7 @@ require("pinnacle").setup(function(pinnacle) op:add_tags("1", "2", "3", "4", "5") -- Same as tag.add(op, "1", "2", "3", "4", "5") - tag.toggle({ "1", op }) + tag.toggle({ name = "1", output = op }) -- Window rules window.rules.add({ @@ -98,9 +98,27 @@ require("pinnacle").setup(function(pinnacle) rule = { size = { 300, 300 }, location = { 50, 50 } }, }, { cond = { - cond_all = { { class = "XTerm" }, { tag = "4" } }, + class = "XTerm", + tag = "4", }, - rule = { size = { 500, 500 }, floating_or_tiled = "Floating" }, + rule = { size = { 500, 800 }, floating_or_tiled = "Floating" }, + }) + + window.rules.add({ + cond = { + cond_all = { + class = "Alacritty", + cond_any = { + { + cond_all = { + tag = { "3", "4", "5" }, + }, + }, + { cond_all = { tag = { "1", "2" } } }, + }, + }, + }, + rule = { floating_or_tiled = "Floating" }, }) end) diff --git a/api/lua/tag.lua b/api/lua/tag.lua index d1dde96..b00179c 100644 --- a/api/lua/tag.lua +++ b/api/lua/tag.lua @@ -41,8 +41,9 @@ local tag_module = {} ---| "CornerBottomLeft" # One main corner window in the bottom left with a column of windows on the right and a row on the top. ---| "CornerBottomRight" # One main corner window in the bottom right with a column of windows on the left and a row on the top. ----@alias TagTable { [1]: string, [2]: (string|Output)? } ----@alias TagTableNamed { name: string, output: (string|Output)? } +---@alias TagTable { name: string, output: (string|Output)? } + +---@alias TagConstructor Tag|TagTable|string ---A tag object. --- @@ -52,92 +53,6 @@ local tag_module = {} ---@field private _id integer The internal id of this tag. local tag = {} ----@nodoc ----***You probably don't need to use this function.*** ---- ----Create a tag from `Tag|TagTable|TagTableNamed|string`. ----@param tb Tag|TagTable|TagTableNamed|string ----@return Tag|nil -function tag_module.create_tag_from_params(tb) - -- If creating from a tag object, just return the obj - if tb.id then - return tb --[[@as Tag]] - end - - -- string passed in - if type(tb) == "string" then - local op = require("output").get_focused() - if op == nil then - return nil - end - - local tags = tag_module.get_by_name(tb) - for _, t in pairs(tags) do - if t:output() and t:output():name() == op:name() then - return t - end - end - - return nil - end - - -- TagTable was passed in - local tag_name = tb[1] - if type(tag_name) == "string" then - local op = tb[2] - if op == nil then - local o = require("output").get_focused() - if o == nil then - return nil - end - op = o - elseif type(op) == "string" then - local o = require("output").get_by_name(op) - if o == nil then - return nil - end - op = o - end - - local tags = tag_module.get_by_name(tag_name) - for _, t in pairs(tags) do - if t:output() and t:output():name() == op:name() then - return t - end - end - - return nil - end - - -- TagTableNamed was passed in - local tb = tb --[[@as TagTableNamed]] - local tag_name = tb.name - local op = tb.output - - if op == nil then - local o = require("output").get_focused() - if o == nil then - return nil - end - op = o - elseif type(op) == "string" then - local o = require("output").get_by_name(op) - if o == nil then - return nil - end - op = o - end - - local tags = tag_module.get_by_name(tag_name) - for _, t in pairs(tags) do - if t:output() and t:output():name() == op:name() then - return t - end - end - - return nil -end - ---Create a tag from an id. ---The id is the unique identifier for each tag. ---@param id TagId @@ -265,10 +180,10 @@ end ---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1" ---tag.toggle(t) ---``` ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Tag.toggle — The corresponding object method function tag_module.toggle(t) - local t = tag_module.create_tag_from_params(t) + local t = tag_module.get(t) if t then SendMsg({ @@ -302,10 +217,10 @@ end ---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1" ---tag.switch_to(t) ---``` ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Tag.switch_to — The corresponding object method function tag_module.switch_to(t) - local t = tag_module.create_tag_from_params(t) + local t = tag_module.get(t) if t then SendMsg({ @@ -323,25 +238,20 @@ end ---local op = output.get_by_name("DP-1") --- ---tag.set_layout("1", "Dwindle") -- Set tag 1 on the focused output to "Dwindle" ----tag.set_layout({ "1" }, "Dwindle") -- Same as above --- ----tag.set_layout({ "1", "DP-1" }, "Dwindle") -- Set tag 1 on DP-1 to "Dwindle" ----tag.set_layout({ "1", op }, "Dwindle") -- Same as above ---- ------ Verbose versions of the two above ----tag.set_layout({ name = "1", output = "DP-1" }, "Dwindle") ----tag.set_layout({ name = "1", output = op }, "Dwindle") +---tag.set_layout({ name = "1", output = "DP-1" }, "Dwindle") -- Set tag 1 on "DP-1" to "Dwindle" +---tag.set_layout({ name = "1", output = op }, "Dwindle") -- Same as above --- ----- Using a tag object ---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1" ---tag.set_layout(t, "Dwindle") ---``` --- ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@param layout Layout The layout. ---@see Tag.set_layout — The corresponding object method function tag_module.set_layout(t, layout) - local t = tag_module.create_tag_from_params(t) + local t = tag_module.get(t) if t then SendMsg({ @@ -364,22 +274,70 @@ end ---### Examples ---```lua ---local t = tag.get("1") ----local t = tag.get({ "1", "HDMI-A-0" }) ---local t = tag.get({ name = "3" }) +---local t = tag.get({ name = "1", output = "HDMI-A-0" }) --- ---local op = output.get_by_name("DP-2") ---if op ~= nil then --- local t = tag.get({ name = "Code", output = op }) ---end ---``` ----@param params TagTable|TagTableNamed|string +---@param params TagConstructor ---@return Tag|nil --- ---@see TagModule.get_on_output ---@see TagModule.get_by_name ---@see TagModule.get_all function tag_module.get(params) - return tag_module.create_tag_from_params(params) + -- If creating from a tag object, just return the obj + if params.id then + return params --[[@as Tag]] + end + + -- string passed in + if type(params) == "string" then + local op = require("output").get_focused() + if op == nil then + return nil + end + + local tags = tag_module.get_by_name(params) + for _, t in pairs(tags) do + if t:output() and t:output():name() == op:name() then + return t + end + end + + return nil + end + + -- TagTable was passed in + local params = params --[[@as TagTable]] + local tag_name = params.name + local op = params.output + + if op == nil then + local o = require("output").get_focused() + if o == nil then + return nil + end + op = o + elseif type(op) == "string" then + local o = require("output").get_by_name(op) + if o == nil then + return nil + end + op = o + end + + local tags = tag_module.get_by_name(tag_name) + for _, t in pairs(tags) do + if t:output() and t:output():name() == op:name() then + return t + end + end + + return nil end ---Get all tags on the specified output. diff --git a/api/lua/window.lua b/api/lua/window.lua index 50c1855..687e8fb 100644 --- a/api/lua/window.lua +++ b/api/lua/window.lua @@ -55,7 +55,7 @@ end --- ---See `WindowModule.move_to_tag` for examples. --- ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see WindowModule.move_to_tag — The corresponding module function function window:move_to_tag(t) window_module.move_to_tag(self, t) @@ -66,7 +66,7 @@ end ---Note: toggling off all tags currently makes a window not respond to layouting. --- ---See `WindowModule.toggle_tag` for examples. ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see WindowModule.toggle_tag — The corresponding module function function window:toggle_tag(t) window_module.toggle_tag(self, t) @@ -254,10 +254,10 @@ end ---Toggle the tag with the given name and (optional) output for the specified window. --- ---@param w Window ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Window.toggle_tag — The corresponding object method function window_module.toggle_tag(w, t) - local t = require("tag").create_tag_from_params(t) + local t = require("tag").get(t) if t then SendMsg({ @@ -272,10 +272,10 @@ end ---Move the specified window to the tag with the given name and (optional) output. --- ---@param w Window ----@param t Tag|TagTable|TagTableNamed|string +---@param t TagConstructor ---@see Window.move_to_tag — The corresponding object method function window_module.move_to_tag(w, t) - local t = require("tag").create_tag_from_params(t) + local t = require("tag").get(t) if t then SendMsg({ diff --git a/api/lua/window_rules.lua b/api/lua/window_rules.lua index 3bf5f16..9b578f0 100644 --- a/api/lua/window_rules.lua +++ b/api/lua/window_rules.lua @@ -2,33 +2,65 @@ ---@class WindowRules local window_rules = {} ----Convert all tag constructors in `cond` to actual tags +---Convert all tag constructors in `cond` to tag ids for serialization. ---@param cond WindowRuleCondition ---@return _WindowRuleCondition local function convert_tag_params(cond) if cond.tag then - ---@type TagId|Tag|nil - local tag = require("tag").create_tag_from_params(cond.tag) - if tag then - ---@diagnostic disable-next-line - tag = tag:id() + local tags = {} + + if type(cond.tag) == "table" then + if cond.tag.name or cond.tag.output then + -- Tag constructor + local tag = require("tag").get(cond.tag) + if tag then + table.insert(tags, tag:id()) + end + else + -- Array of tag constructors + ---@diagnostic disable-next-line + for _, t in pairs(cond.tag) do + local tag = require("tag").get(t) + if tag then + table.insert(tags, tag:id()) + end + end + end + else + -- Tag constructor + local tag = require("tag").get(cond.tag) + if tag then + table.insert(tags, tag:id()) + end end - ---@diagnostic disable-next-line - cond.tag = tag + + cond.tag = tags end if cond.cond_any then local conds = {} - for _, c in pairs(cond.cond_any) do - table.insert(conds, convert_tag_params(c)) + if type(cond.cond_any[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_any) do + table.insert(conds, convert_tag_params(c)) + end + else + -- Single cond + table.insert(conds, convert_tag_params(cond.cond_any)) end cond.cond_any = conds end if cond.cond_all then local conds = {} - for _, c in pairs(cond.cond_all) do - table.insert(conds, convert_tag_params(c)) + if type(cond.cond_all[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_all) do + table.insert(conds, convert_tag_params(c)) + end + else + -- Single cond + table.insert(conds, convert_tag_params(cond.cond_all)) end cond.cond_all = conds end @@ -36,6 +68,53 @@ local function convert_tag_params(cond) return cond --[[@as _WindowRuleCondition]] end +---These attributes need to be arrays, so this function converts single values into arrays. +---@param cond WindowRuleCondition +---@return WindowRuleCondition +local function convert_single_attrs(cond) + if type(cond.class) == "string" then + -- stylua: ignore start + cond.class = { cond.class --[[@as string]] } + -- stylua: ignore end + end + + if type(cond.title) == "string" then + -- stylua: ignore start + cond.title = { cond.title --[[@as string]] } + -- stylua: ignore end + end + + if cond.cond_any then + local conds = {} + if type(cond.cond_any[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_any) do + table.insert(conds, convert_single_attrs(c)) + end + else + -- Single cond + table.insert(conds, convert_single_attrs(cond.cond_any)) + end + cond.cond_any = conds + end + + if cond.cond_all then + local conds = {} + if type(cond.cond_all[1]) == "table" then + -- Array of conds + for _, c in pairs(cond.cond_all) do + table.insert(conds, convert_single_attrs(c)) + end + else + -- Single cond + table.insert(conds, convert_single_attrs(cond.cond_all)) + end + cond.cond_all = conds + end + + return cond +end + ---Add one or more window rules. --- ---A window rule defines what a window will spawn with given certain conditions. @@ -45,8 +124,15 @@ end --- - `cond`: The condition for `rule` to apply to a new window. --- - `rule`: What gets applied to the new window if `cond` is true. --- ----`cond` can be a bit confusing and *very* table heavy. Examples are shown below for guidance. ----An attempt at simplifying this API will happen in the future, but is a low priority. +---There are some important mechanics you should know when using window rules: +--- +--- - All children inside a `cond_all` block must be true for the block to be true. +--- - At least one child inside a `cond_any` block must be true for the block to be true. +--- - The outermost block of a window rule condition is implicitly a `cond_all` block. +--- - All condition attributes (`tag`, `title`, `class`, etc.) can either be a single value or an array. +--- This includes `cond_all` and `cond_any`. +--- +---`cond` can be a bit confusing and quite table heavy. Examples are shown below for guidance. --- ---### Examples ---```lua @@ -58,28 +144,45 @@ end --- ----- To apply rules when *all* provided conditions are true, use `cond_all`. ----- `cond_all` takes an array of conditions and checks if all are true. ------ Note that `cond_any` is not a keyed table; rather, it's a table of tables. ---- ----- The following will open Steam fullscreen only if it opens on tag "5". ---window.rules.add({ --- cond = { ---- cond_any = { ---- { class = "steam" }, -- Note that each table must only have one key. ---- { tag = tag.get_by_name("5")[1] }, +--- cond_all = { +--- class = "steam", +--- tag = tag.get("5"), --- } --- }, --- rule = { fullscreen_or_maximized = "Fullscreen" }, ---}) --- +----- The outermost block of a `cond` is implicitly a `cond_all`. +----- Thus, the above can be shortened to: +---window.rules.add({ +--- cond = { +--- class = "steam", +--- tag = tag.get("5"), +--- }, +--- rule = { fullscreen_or_maximized = "Fullscreen" }, +---}) +--- +----- `cond_any` also exists to allow at least one provided condition to match. +----- The following will open either xterm or Alacritty floating. +---window.rules.add({ +--- cond = { +--- cond_any = { class = { "xterm", "Alacritty" } } +--- }, +--- rule = { floating_or_tiled = "Floating" } +---}) +--- ----- You can arbitrarily nest `cond_any` and `cond_all` to achieve desired logic. ------ The following will open Discord, Thunderbird, or Alacritty floating if they +----- The following will open Discord, Thunderbird, or Firefox floating if they ----- open on either *all* of tags "A", "B", and "C" or both tags "1" and "2". ---window.rules.add({ ---- cond = { cond_all = { ---- { cond_any = { { class = "discord" }, { class = "firefox" }, { class = "thunderbird" } } }, +--- cond = { cond_all = { -- This outer `cond_all` block is unnecessary, but it's here for clarity. +--- { cond_any = { class = { "firefox", "thunderbird", "discord" } } }, --- { cond_any = { ---- { cond_all = { { tag = "A" }, { tag = "B" }, { tag = "C" } } }, ---- { cond_all = { { tag = "1" }, { tag = "2" } } }, +--- { cond_all = { tag = { "A", "B", "C" } } }, +--- { cond_all = { tag = { "1", "2" } } }, --- } } --- } }, --- rule = { floating_or_tiled = "Floating" }, @@ -90,13 +193,15 @@ function window_rules.add(...) local rules = { ... } for _, rule in pairs(rules) do + rule.cond = convert_single_attrs(rule.cond) + ---@diagnostic disable-next-line rule.cond = convert_tag_params(rule.cond) if rule.rule.tags then local tags = {} for _, tag in pairs(rule.rule.tags) do - local t = require("tag").create_tag_from_params(tag) + local t = require("tag").get(tag) if t then ---@diagnostic disable-next-line t = t:id() diff --git a/api/lua/window_rules_types.lua b/api/lua/window_rules_types.lua index f9e7cda..45b3137 100644 --- a/api/lua/window_rules_types.lua +++ b/api/lua/window_rules_types.lua @@ -8,19 +8,19 @@ ---@class _WindowRuleCondition ---@field cond_any _WindowRuleCondition[]? At least one provided condition must be true. ---@field cond_all _WindowRuleCondition[]? All provided conditions must be true. ----@field class string? The window must have this class. ----@field title string? The window must have this title. ----@field tag TagId? The window must be on this tag. +---@field class string[]? The window must have this class. +---@field title string[]? The window must have this title. +---@field tag TagId[]? The window must be on this tag. ---Conditions for window rules. Only one condition can be in the table. ---If you have more than one you need to check for, use `cond_any` or `cond_all` ---to check for any or all conditions. ---@class WindowRuleCondition ----@field cond_any WindowRuleCondition[]? At least one provided condition must be true. ----@field cond_all WindowRuleCondition[]? All provided conditions must be true. ----@field class string? The window must have this class. ----@field title string? The window must have this title. ----@field tag (Tag|TagTable|TagTableNamed|string)? The window must be on this tag. +---@field cond_any (WindowRuleCondition|WindowRuleCondition[])? At least one provided condition must be true. +---@field cond_all (WindowRuleCondition|WindowRuleCondition[])? All provided conditions must be true. +---@field class (string|string[])? The window must have this class. +---@field title (string|string[])? The window must have this title. +---@field tag (TagConstructor|TagConstructor[])? The window must be on this tag. ---@class _WindowRule Attributes the window will be spawned with. ---@field output OutputName? The output this window will be spawned on. TODO: @@ -32,7 +32,7 @@ ---@class WindowRule Attributes the window will be spawned with. ---@field output (Output|OutputName)? The output this window will be spawned on. TODO: ----@field tags (Tag|TagTable|TagTableNamed|string)[]? The tags this window will be spawned with. +---@field tags TagConstructor[]? The tags this window will be spawned with. ---@field floating_or_tiled ("Floating"|"Tiled")? Whether or not this window will be spawned floating or tiled. ---@field fullscreen_or_maximized FullscreenOrMaximized? Whether or not this window will be spawned fullscreen, maximized, or forced to neither. ---@field size { [1]: integer, [2]: integer }? The size the window will spawn with, with [1] being width and [2] being height. This must be a strictly positive integer; putting 0 will crash the compositor. diff --git a/src/api/msg/window_rules.rs b/src/api/msg/window_rules.rs index 90d2700..9fa75c4 100644 --- a/src/api/msg/window_rules.rs +++ b/src/api/msg/window_rules.rs @@ -10,39 +10,123 @@ use crate::{ }; #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum WindowRuleCondition { +pub struct WindowRuleCondition { /// This condition is met when any of the conditions provided is met. - CondAny(Vec), + #[serde(default)] + cond_any: Option>, /// This condition is met when all of the conditions provided are met. - CondAll(Vec), + #[serde(default)] + cond_all: Option>, /// This condition is met when the class matches. - Class(String), + #[serde(default)] + class: Option>, /// This condition is met when the title matches. - Title(String), + #[serde(default)] + title: Option>, /// This condition is met when the tag matches. - Tag(TagId), + #[serde(default)] + tag: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum AllOrAny { + All, + Any, } impl WindowRuleCondition { /// RefCell Safety: This method uses RefCells on `window`. pub fn is_met(&self, state: &State, window: &WindowElement) -> bool { - match self { - WindowRuleCondition::CondAny(conds) => { - conds.iter().any(|cond| Self::is_met(cond, state, window)) - } - WindowRuleCondition::CondAll(conds) => { - conds.iter().all(|cond| Self::is_met(cond, state, window)) - } - WindowRuleCondition::Class(class) => window.class().as_ref() == Some(class), - WindowRuleCondition::Title(title) => window.title().as_ref() == Some(title), - WindowRuleCondition::Tag(tag) => { - let Some(tag) = tag.tag(state) else { - tracing::warn!("WindowRuleCondition no tag"); - return false; + Self::is_met_inner(self, state, window, AllOrAny::All) + } + + fn is_met_inner(&self, state: &State, window: &WindowElement, all_or_any: AllOrAny) -> bool { + tracing::debug!("{self:#?}"); + + let WindowRuleCondition { + cond_any, + cond_all, + class, + title, + tag, + } = self; + + match all_or_any { + AllOrAny::All => { + let cond_any = if let Some(cond_any) = cond_any { + cond_any + .iter() + .any(|cond| Self::is_met_inner(cond, state, window, AllOrAny::Any)) + } else { + true + }; + let cond_all = if let Some(cond_all) = cond_all { + cond_all + .iter() + .all(|cond| Self::is_met_inner(cond, state, window, AllOrAny::All)) + } else { + true + }; + let classes = if let Some(classes) = class { + classes + .iter() + .all(|class| window.class().as_ref() == Some(class)) + } else { + true + }; + let titles = if let Some(titles) = title { + titles + .iter() + .all(|title| window.title().as_ref() == Some(title)) + } else { + true + }; + let tags = if let Some(tag_ids) = tag { + let mut tags = tag_ids.iter().filter_map(|tag_id| tag_id.tag(state)); + tags.all(|tag| window.with_state(|state| state.tags.contains(&tag))) + } else { + true }; - window.with_state(|state| state.tags.contains(&tag)) + tracing::debug!("{cond_all} {cond_any} {classes} {titles} {tags}"); + cond_all && cond_any && classes && titles && tags + } + AllOrAny::Any => { + let cond_any = if let Some(cond_any) = cond_any { + cond_any + .iter() + .any(|cond| Self::is_met_inner(cond, state, window, AllOrAny::Any)) + } else { + false + }; + let cond_all = if let Some(cond_all) = cond_all { + cond_all + .iter() + .all(|cond| Self::is_met_inner(cond, state, window, AllOrAny::All)) + } else { + false + }; + let classes = if let Some(classes) = class { + classes + .iter() + .any(|class| window.class().as_ref() == Some(class)) + } else { + false + }; + let titles = if let Some(titles) = title { + titles + .iter() + .any(|title| window.title().as_ref() == Some(title)) + } else { + false + }; + let tags = if let Some(tag_ids) = tag { + let mut tags = tag_ids.iter().filter_map(|tag_id| tag_id.tag(state)); + tags.any(|tag| window.with_state(|state| state.tags.contains(&tag))) + } else { + false + }; + cond_all || cond_any || classes || titles || tags } } } diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index 6394f8f..1bd23d0 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -224,12 +224,6 @@ impl XwmHandler for CalloopData { .cloned(); if let Some(win) = win { self.state.space.unmap_elem(&win); - // self.state.windows.retain(|elem| &win != elem); - // if win.with_state(|state| state.floating.is_tiled()) { - // if let Some(output) = win.output(&self.state) { - // self.state.re_layout(&output); - // } - // } } if !window.is_override_redirect() { tracing::debug!("set mapped to false"); @@ -290,7 +284,6 @@ impl XwmHandler for CalloopData { geometry: Rectangle, _above: Option, ) { - // tracing::debug!("x11 configure_notify"); let Some(win) = self .state .space