From d8b53dac5d285b8b5b12ac2572575c73c5be1af5 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 10 May 2019 17:48:10 -0400 Subject: [PATCH 01/12] matcher: Add methods to add new rules. It is now possible to add and remove rules. This is superior to how `awful.rules` originally handled rules because modules can now assume adding and removing rules works. The reason for the methods rather than `table.insert` is partially because future commits will add signals. In turn, this will allow `gears.matcher` to be extended by module using it using the extra "introspection" made possible by the signals. --- lib/gears/matcher.lua | 50 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index fac9ffc1..d371514e 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -100,10 +100,19 @@ end -- -- @param o The object. -- @tparam[opt=nil] table rules The rules to check. List with "rule", "rule_any", --- "except" and "except_any" keys. +-- "except" and "except_any" keys. If no rules are provided, one is selected at +-- random. Unless more rule sources are added, there is only one to begin with. -- @treturn table The list of matched rules. function matcher:matching_rules(o, rules) + rules = rules or select(2, next(self._matching_rules)) + local result = {} + + if not rules then + gdebug.print_warning("This matcher has no rule source") + return result + end + for _, entry in ipairs(rules) do if self:matches_rule(o, entry) then table.insert(result, entry) @@ -288,6 +297,45 @@ function matcher:_execute(o, props, callbacks) end end +--- Add a new rule to the default set. +-- @tparam string source The source name. +-- @tparam table rule A valid rule. +-- @method append_rule +function matcher:append_rule(source, rule) + if not self._matching_rules[source] then + self:add_matching_rules(source, {}, {}, {}) + end + table.insert(self._matching_rules[source], rule) +end + +--- Add a new rules to the default set. +-- @tparam string source The source name. +-- @tparam table rules A table with rules. +-- @method append_rules +function matcher:append_rules(source, rules) + for _, rule in ipairs(rules) do + self:append_rule(source, rule) + end +end + +--- Remove a new rule to the default set. +-- @tparam string source The source name. +-- @tparam table rule A valid rule. +-- @treturn boolean If the rule was removed. +-- @method remove_rule +function matcher:remove_rule(source, rule) + if not self._matching_rules[source] then return end + + for k, v in ipairs(self._matching_rules[source]) do + if v == rule then + table.remove(self._matching_rules[source], k) + return true + end + end + + return false +end + local module = {} --- Create a new rule solver object. From 886f8ea4e3135979ccc39817b1a026de5ab80e54 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 10 May 2019 19:53:58 -0400 Subject: [PATCH 02/12] matcher: Use gears.object. This way there's some signals. It can be useful if the module using the matcher needs to act when something happens. --- lib/gears/matcher.lua | 47 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index d371514e..21a55025 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -35,10 +35,45 @@ local gtable = require("gears.table") local gsort = require("gears.sort") local gdebug = require("gears.debug") +local gobject = require("gears.object") local protected_call = require("gears.protected_call") local matcher = {} +--- A rule has been added to a set of matching rules. +-- @signal rule::appended +-- @tparam table gears.matcher The matcher. +-- @tparam table rule The rule. +-- @tparam table source The matching rules source name. +-- @tparam table content The matching rules source content. +-- @see append_rule +-- @see append_rules + +--- A rule has been removed to a set of matching rules. +-- @signal rule::removed +-- @tparam table gears.matcher The matcher. +-- @tparam table rule The rule. +-- @tparam table source The matching rules source name. +-- @tparam table content The matching rules source content. +-- @see remove_rule + +--- A matching source function has been added. +-- @signal matching_function::added +-- @tparam table gears.matcher The matcher. +-- @tparam function callback The callback. +-- @see add_matching_function + +--- A matching source table has been added. +-- @signal matching_rules::added +-- @tparam table gears.matcher The matcher. +-- @tparam function callback The callback. +-- @see add_matching_rules + +--- A matching source function has been removed. +-- @signal matching_source::removed +-- @tparam table gears.matcher The matcher. +-- @see remove_matching_source + -- Check if an object matches a rule. -- @param o The object. -- #tparam table rule The rule to check. @@ -162,6 +197,8 @@ function matcher:add_matching_rules(name, rules, depends_on, precede) self._matching_rules[name] = rules + self:emit_signal("matching_rules::added", rules) + return self:add_matching_function(name, matching_fct, depends_on, precede) end @@ -230,6 +267,8 @@ function matcher:add_matching_function(name, callback, depends_on, precede) end end + self:emit_signal("matching_function::added", callback) + return true end @@ -245,11 +284,13 @@ function matcher:remove_matching_source(name) for k, v in ipairs(self._matching_source) do if v.name == name then + self:emit_signal("matching_source::removed", v) table.remove(self._matching_source, k) return true end end + self._matching_rules[name] = nil return false @@ -306,6 +347,7 @@ function matcher:append_rule(source, rule) self:add_matching_rules(source, {}, {}, {}) end table.insert(self._matching_rules[source], rule) + self:emit_signal("rule::appended", rule, source, self._matching_rules[source]) end --- Add a new rules to the default set. @@ -329,6 +371,7 @@ function matcher:remove_rule(source, rule) for k, v in ipairs(self._matching_rules[source]) do if v == rule then table.remove(self._matching_rules[source], k) + self:emit_signal("rule::removed", rule, source, self._matching_rules[source]) return true end end @@ -343,7 +386,9 @@ local module = {} -- @return A new rule solver object. local function new() - local ret = {} + local ret = gobject() + + rawset(ret, "_private", {rules = {}}) -- Contains the sources. -- The elements are ordered "first in, first executed". Thus, the higher the From f3f9e4a4b5657858fa5ed4e5525336ee39f1a6e4 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 25 May 2019 10:07:00 -0400 Subject: [PATCH 03/12] matcher: Allow rules to be addressed using an identifier. --- docs/common/rule.ldoc | 12 ++++++++++++ lib/gears/matcher.lua | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/common/rule.ldoc b/docs/common/rule.ldoc index 1d2c0fc8..776e50bd 100644 --- a/docs/common/rule.ldoc +++ b/docs/common/rule.ldoc @@ -44,3 +44,15 @@ -- @param table -- @see rule -- @see except + +--- An identifier for this rule. +-- +-- It can be anything. It will be compared with the `==` operator. Strings are +-- highly recommended. +-- +-- Setting an `id` is useful to be able to remove the rule by using its id +-- instead of a table reference. Modules can also listen to `rule::appended` and +-- modify or disable a rule. +-- +-- @rulecomponent id +-- @param table|string|number|function diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index 21a55025..8b693855 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -362,14 +362,14 @@ end --- Remove a new rule to the default set. -- @tparam string source The source name. --- @tparam table rule A valid rule. +-- @tparam string|table rule An existing rule or its `id`. -- @treturn boolean If the rule was removed. -- @method remove_rule function matcher:remove_rule(source, rule) if not self._matching_rules[source] then return end for k, v in ipairs(self._matching_rules[source]) do - if v == rule then + if v == rule or v.id == rule then table.remove(self._matching_rules[source], k) self:emit_signal("rule::removed", rule, source, self._matching_rules[source]) return true From 9a16ee62e64c8d6633998550d3b07c4897402919 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 4 Jun 2019 00:03:35 -0400 Subject: [PATCH 04/12] matcher: Add a way to match properties beside == and patterns. It is useful for objects and avoid the mess that it Lua == overload. The primary use case will be to match tags by name or object. --- lib/gears/matcher.lua | 53 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index 8b693855..b77bc150 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -81,7 +81,12 @@ local matcher = {} function matcher:_match(o, rule) if not rule then return false end for field, value in pairs(rule) do - if o[field] then + local pm = self._private.prop_matchers[field] + if pm then + if not pm(o, value, field) then + return false + end + elseif o[field] ~= nil then if type(o[field]) == "string" then if not o[field]:match(value) and o[field] ~= value then return false @@ -105,7 +110,10 @@ function matcher:_match_any(o, rule) for field, values in pairs(rule) do if o[field] then for _, value in ipairs(values) do - if o[field] == value then + local pm = self._private.prop_matchers[field] + if pm and pm(o, value, field) then + return true + elseif o[field] == value then return true elseif type(o[field]) == "string" and o[field]:match(value) then return true @@ -170,6 +178,45 @@ function matcher:matches_rules(o, rules) return false end +--- Assign a function to match an object property against a value. +-- +-- The default matcher uses the `==` operator for all types. It also uses the +-- `:match()` method for string and allows pattern matching. If the value is a +-- function, then that function is called with the object and the current +-- properties to be applied. If the function returns true, the match is +-- accepted. +-- +-- Custom property matcher are useful when objects are compared. This avoids +-- having to implement custom metatable for everything. +-- +-- The `f` function receives 3 arguments: +-- +-- * The object to match against (anything) +-- * The value to compare +-- * The property/field name. +-- +-- It should return `true` if it matches and `false` otherwise. +-- +-- @tparam string name The property name. +-- @tparam function f The matching function. +-- @method add_property_matcher +-- @usage -- Manually match the screen in various ways. +-- matcher:add_property_matcher("screen", function(c, value) +-- return c.screen == value +-- or screen[c.screen] == value +-- or c.screen.outputs[value] ~= nil +-- or value == "any" +-- or (value == "primary" and c.screen == screen.primary) +-- end) +-- +function matcher:add_property_matcher(name, f) + assert(not self._private.prop_matchers[name], name .. " already has a matcher") + + self._private.prop_matchers[name] = f + + self:emit_signal("property_matcher::added", name, f) +end + local function default_rules_callback(self, o, props, callbacks, rules) for _, entry in ipairs(self:matching_rules(o, rules)) do gtable.crush(props, entry.properties or {}) @@ -388,7 +435,7 @@ local module = {} local function new() local ret = gobject() - rawset(ret, "_private", {rules = {}}) + rawset(ret, "_private", { rules = {}, prop_matchers = {} }) -- Contains the sources. -- The elements are ordered "first in, first executed". Thus, the higher the From 801ae69f237d87045461453de8402c10cde8419c Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Wed, 17 Jul 2019 22:06:04 -0400 Subject: [PATCH 05/12] matcher: Add a custom setter for "fake" object properties. This is hardcoded in `awful.rules`, but cannot be shared due to the priority corner cases. Given in the long run any "standard" priority should use the topological sort API, better not try to share *that* code. --- lib/gears/matcher.lua | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index b77bc150..c083c8f8 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -217,6 +217,22 @@ function matcher:add_property_matcher(name, f) self:emit_signal("property_matcher::added", name, f) end +--- Add a special setter for a property. +-- +-- This is useful to add more properties to object which only make sense within +-- the context of a rule. +-- +-- @method add_property_setter +-- @tparam string name The property name. +-- @tparam function f The setter function. +function matcher:add_property_setter(name, f) + assert(not self._private.prop_setters[name], name .. " already has a matcher") + + self._private.prop_setters[name] = f + + self:emit_signal("property_setter::added", name, f) +end + local function default_rules_callback(self, o, props, callbacks, rules) for _, entry in ipairs(self:matching_rules(o, rules)) do gtable.crush(props, entry.properties or {}) @@ -377,7 +393,9 @@ function matcher:_execute(o, props, callbacks) value = value(o, props) end - if type(o[property]) == "function" then + if self._private.prop_setters[property] then + self._private.prop_setters[property](o, value) + elseif type(o[property]) == "function" then o[property](o, value) else o[property] = value @@ -435,7 +453,9 @@ local module = {} local function new() local ret = gobject() - rawset(ret, "_private", { rules = {}, prop_matchers = {} }) + rawset(ret, "_private", { + rules = {}, prop_matchers = {}, prop_setters = {} + }) -- Contains the sources. -- The elements are ordered "first in, first executed". Thus, the higher the From 774465df4d319b195b8333aa69546221a8edb758 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Wed, 17 Jul 2019 23:55:10 -0400 Subject: [PATCH 06/12] doc: Add the new grears.matcher features to the lone example. This isn't really intended to be used outside of the object rules. --- tests/examples/text/gears/matcher/default.lua | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/tests/examples/text/gears/matcher/default.lua b/tests/examples/text/gears/matcher/default.lua index ec60bc96..38da3216 100644 --- a/tests/examples/text/gears/matcher/default.lua +++ b/tests/examples/text/gears/matcher/default.lua @@ -11,7 +11,8 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE -- This rule will match local rule1 = { rule = { - answer = 42, + answer = 42, + everything = true, }, properties = { name = "baz", @@ -69,6 +70,41 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE -- This will add the `rules` to this matcher. matcher:add_matching_rules("second", rules, {"first"}, {}) +--DOC_NEWLINE + -- Some properties cannot be checked with the `==` operator (like those + -- with multiple possible types). In that case, it is possible to define + -- special comparator function. + matcher:add_property_matcher("everything", function(obj, value) + return value and obj.answer == 42 + end) + +--DOC_NEWLINE + + -- The same can be done for the property section. + matcher:add_property_setter("multiply_by", function(obj, value) + obj.answer = (obj.answer or 1) * value + end) + +--DOC_NEWLINE + + -- It is possible to append rules to existing (or new) sources. + matcher:append_rule( "second", { + id = "rule_with_id", + rule = { + has_elite = true, + }, + properties = { + multiply_by = "1337", + }, + }) + +--DOC_NEWLINE + + -- Or remove them. + local rm3 = --DOC_HIDE + matcher:remove_rule("second", "rule_with_id") + assert(rm3) --DOC_HIDE + --DOC_NEWLINE -- Apply the properties to `o` @@ -76,5 +112,12 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE assert(o.is_everything) --DOC_HIDE assert(o.name == "baz") --DOC_HIDE +local rm1 = --DOC_HIDE matcher:remove_matching_source("first") --DOC_HIDE +assert(rm1) --DOC_HIDE + +matcher:append_rules("second", {{},{},{}}) --DOC_HIDE + +local rm2 = --DOC_HIDE matcher:remove_matching_source("second") --DOC_HIDE +assert(rm2) --DOC_HIDE From 3e4f29290613e147c83b31151fd22b312450bbef Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Fri, 19 Jul 2019 20:00:25 -0400 Subject: [PATCH 07/12] matcher: Fix the doc. It was developed in parallel to the new doc format and wasn't updated. --- lib/gears/matcher.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index c083c8f8..036da00a 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -129,6 +129,7 @@ end -- @tparam table entry Rule entry (with keys `rule`, `rule_any`, `except` and/or -- `except_any`). -- @treturn boolean If `o` matches `entry`. +-- @method matches_rule function matcher:matches_rule(o, entry) local match = self:_match(o, entry.rule) or self:_match_any(o, entry.rule_any) return match @@ -146,6 +147,7 @@ end -- "except" and "except_any" keys. If no rules are provided, one is selected at -- random. Unless more rule sources are added, there is only one to begin with. -- @treturn table The list of matched rules. +-- @method matching_rules function matcher:matching_rules(o, rules) rules = rules or select(2, next(self._matching_rules)) @@ -169,6 +171,7 @@ end -- @tparam table rules The rules to check. List of tables with `rule`, -- `rule_any`, `except` and `except_any` keys. -- @treturn boolean True if at least one rule is matched, false otherwise. +-- @method matches_rules function matcher:matches_rules(o, rules) for _, entry in ipairs(rules) do if self:matches_rule(o, entry) then @@ -253,6 +256,7 @@ end -- @tparam[opt={}] table precede A list of names of sources this source has a -- priority over. -- @treturn boolean Returns false if a dependency conflict was found. +-- @method add_matching_rules function matcher:add_matching_rules(name, rules, depends_on, precede) local function matching_fct(_self, c, props, callbacks) default_rules_callback(_self, c, props, callbacks, rules) @@ -280,6 +284,7 @@ end -- @tparam[opt={}] table precede A list of names of sources this source has a -- priority over. -- @treturn boolean Returns false if a dependency conflict was found. +-- @method add_matching_function function matcher:add_matching_function(name, callback, depends_on, precede) depends_on = depends_on or {} precede = precede or {} @@ -342,6 +347,7 @@ end -- -- @tparam string name The source name. -- @treturn boolean If the source has been removed. +-- @method remove_matching_source function matcher:remove_matching_source(name) self._rule_source_sort:remove(name) @@ -365,6 +371,7 @@ end -- and rules. -- -- @param o The object. +-- @method apply function matcher:apply(o) local callbacks, props = {}, {} for _, v in ipairs(self._matching_source) do @@ -447,7 +454,7 @@ end local module = {} --- Create a new rule solver object. --- @function gears.matcher +-- @constructorfct gears.matcher -- @return A new rule solver object. local function new() From b40083780e014a8e3f5bda1afa7411552028ef23 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 21 Jul 2019 22:05:45 -0400 Subject: [PATCH 08/12] matcher: Add a "every" and "every_any" sections to the rules. So far the "any" rules had a "OR" and "NOT" logic "gates", but not an "AND". --- docs/common/rule.ldoc | 9 +++++-- lib/gears/matcher.lua | 59 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/docs/common/rule.ldoc b/docs/common/rule.ldoc index 776e50bd..ec0fa7a5 100644 --- a/docs/common/rule.ldoc +++ b/docs/common/rule.ldoc @@ -2,8 +2,6 @@ --- A table which content will be used to set the target object properties. -- --- foo --- -- @rulecomponent properties -- @param table -- @see callbacks @@ -45,6 +43,13 @@ -- @see rule -- @see except +--- Matches when one of every "category" of components match. +-- +-- @rulecomponent rule_every +-- @param table +-- @see rule +-- @see except + --- An identifier for this rule. -- -- It can be anything. It will be compared with the `==` operator. Strings are diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index 036da00a..9929eaa6 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -25,6 +25,8 @@ -- -- @DOC_text_gears_matcher_default_EXAMPLE@ -- +-- @DOC_text_gears_matcher_types_EXAMPLE@ +-- -- More examples are available in `awful.rules`. -- -- @author Julien Danjou <julien@danjou.info> @@ -101,29 +103,63 @@ function matcher:_match(o, rule) return true end +local function field_matcher(self, o, field, value) + local pm = self._private.prop_matchers[field] + + if pm and pm(o, value, field) then + return true + elseif o[field] == value then + return true + elseif type(o[field]) == "string" and o[field]:match(value) then + return true + end + + return false +end + -- Check if an object matches any part of a rule. -- @param o The object. --- #tparam table rule The rule to check. +-- #tparam table rule The rule _match_anyto check. -- @treturn boolean True if at least one rule is matched, false otherwise. function matcher:_match_any(o, rule) if not rule then return false end for field, values in pairs(rule) do if o[field] then for _, value in ipairs(values) do - local pm = self._private.prop_matchers[field] - if pm and pm(o, value, field) then - return true - elseif o[field] == value then - return true - elseif type(o[field]) == "string" and o[field]:match(value) then - return true - end + if field_matcher(self, o, field, value) then return true end end end end + return false end +-- Check if an object matches at least one of every part of a rule. +-- +-- @param o The object. +-- @tparam table rule The rule _match_anyto check. +-- @tparam boolean multi If the entries are table of choices. +-- @treturn boolean True if all rules are matched. +function matcher:_match_every(o, rule) + if not rule then return true end + + for field, values in pairs(rule) do + local found = false + for _, value in ipairs(values) do + if not field_matcher(self, o, field, value) then + found = true + break + end + end + + if not found then + return false + end + end + + return true +end + --- Does a given rule entry match an object? -- @param o The object. -- @tparam table entry Rule entry (with keys `rule`, `rule_any`, `except` and/or @@ -133,8 +169,9 @@ end function matcher:matches_rule(o, entry) local match = self:_match(o, entry.rule) or self:_match_any(o, entry.rule_any) return match - and (not self:_match(o, entry.except)) - and (not self:_match_any(o, entry.except_any)) + and self:_match_every (o, entry.rule_every) + and (not self:_match (o, entry.except )) + and (not self:_match_any (o, entry.except_any)) end --- Get list of matching rules for an object. From 2dc8c62b9b2e3d369202680b62e812657aa7ac45 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 21 Jul 2019 22:16:06 -0400 Subject: [PATCH 09/12] tests: Test gears.matcher "rule_every". --- tests/examples/text/gears/matcher/types.lua | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/examples/text/gears/matcher/types.lua diff --git a/tests/examples/text/gears/matcher/types.lua b/tests/examples/text/gears/matcher/types.lua new file mode 100644 index 00000000..d4dd3c00 --- /dev/null +++ b/tests/examples/text/gears/matcher/types.lua @@ -0,0 +1,62 @@ +--DOC_HIDE --DOC_NO_USAGE +local gears = {matcher = require("gears.matcher")} --DOC_HIDE + + + local matcher = gears.matcher() + + matcher:append_rule( "my.source", { + rule = { + my_any_rule = true, + }, + rule_every = { + every1 = {1, 42}, + every2 = {2, 42}, + every3 = {3, 42}, + }, + except = { + except1 = 1, + }, + properties = { + was_a_match = true, + }, + }) + + local candidate1 = { + my_any_rule = true, + every1 = 1, + every2 = 2, + every3 = 3, + was_a_match = false, + } + +--DOC_NEWLINE + + local candidate2 = { + every2 = 2, + was_a_match = false, + } + +--DOC_NEWLINE + + local candidate3 = { + my_any_rule = true, + was_a_match = false, + every1 = 1, + every2 = 2, + every3 = 3, + except1 = 1, + } + + +--DOC_NEWLINE + + matcher:apply(candidate1) + matcher:apply(candidate2) + matcher:apply(candidate3) + +--DOC_NEWLINE + + -- Only candidate1 fits all criteria. + assert(candidate1.was_a_match == true ) + assert(candidate2.was_a_match == false) + assert(candidate3.was_a_match == false) From 64bef57013b5d8b1354d1d580f1523f0ab899072 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 28 Jul 2019 18:51:57 -0400 Subject: [PATCH 10/12] matcher: Add a `greater` and `lesser` matching sections. In a perfect world we would have pure expression matching, but that's problematic with all the "metaness" of the code. For now, this adds an imperfect way to match the minimum and maximum of number properties. It will be used by the screen rules for the DPI and size properties. --- lib/gears/matcher.lua | 78 ++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index 9929eaa6..300b26b4 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -76,41 +76,47 @@ local matcher = {} -- @tparam table gears.matcher The matcher. -- @see remove_matching_source +local function default_matcher(a, b) + return a == b or (type(a) == "string" and a:match(b)) +end + +local function greater_matcher(a, b) + return a > b +end + +local function lesser_matcher(a, b) + return a < b +end + -- Check if an object matches a rule. -- @param o The object. -- #tparam table rule The rule to check. -- @treturn boolean True if it matches, false otherwise. -function matcher:_match(o, rule) +-- @method _match +function matcher:_match(o, rule, matcher_f) if not rule then return false end + + matcher_f = matcher_f or default_matcher + for field, value in pairs(rule) do local pm = self._private.prop_matchers[field] - if pm then - if not pm(o, value, field) then - return false - end - elseif o[field] ~= nil then - if type(o[field]) == "string" then - if not o[field]:match(value) and o[field] ~= value then - return false - end - elseif o[field] ~= value then - return false - end - else + if pm and pm(o, value, field) then + return true + elseif not matcher_f(o[field], value) then return false end end return true end -local function field_matcher(self, o, field, value) +local function field_matcher(self, o, field, value, matcher_f) + matcher_f = matcher_f or default_matcher + local pm = self._private.prop_matchers[field] if pm and pm(o, value, field) then return true - elseif o[field] == value then - return true - elseif type(o[field]) == "string" and o[field]:match(value) then + elseif matcher_f(o[field] , value) then return true end @@ -121,12 +127,21 @@ end -- @param o The object. -- #tparam table rule The rule _match_anyto check. -- @treturn boolean True if at least one rule is matched, false otherwise. +-- @method _match_any function matcher:_match_any(o, rule) if not rule then return false end for field, values in pairs(rule) do if o[field] then + + -- Special case, "all" + if type(values) == "boolean" and values then + return true + end + for _, value in ipairs(values) do - if field_matcher(self, o, field, value) then return true end + if field_matcher(self, o, field, value) then + return true + end end end end @@ -140,6 +155,7 @@ end -- @tparam table rule The rule _match_anyto check. -- @tparam boolean multi If the entries are table of choices. -- @treturn boolean True if all rules are matched. +-- @method _match_every function matcher:_match_every(o, rule) if not rule then return true end @@ -168,10 +184,26 @@ end -- @method matches_rule function matcher:matches_rule(o, entry) local match = self:_match(o, entry.rule) or self:_match_any(o, entry.rule_any) - return match - and self:_match_every (o, entry.rule_every) - and (not self:_match (o, entry.except )) - and (not self:_match_any (o, entry.except_any)) + + -- If there was `rule` or `rule_any` and they failed to match, look no further. + if (not match) and (entry.rule or entry.rule_any) then return false end + + if not self:_match_every(o, entry.rule_every) then return false end + + -- Negative matching. + if entry.except and self:_match(o, entry.except) then return false end + if entry.except_any and self:_match_any(o, entry.except_any) then return false end + + -- Other operators. + if entry.rule_greater and not self:_match(o, entry.rule_greater, greater_matcher) then + return false + end + + if entry.rule_lesser and not self:_match(o, entry.rule_lesser, lesser_matcher) then + return false + end + + return true end --- Get list of matching rules for an object. From 91ca922671c15156cccd925508da3b1b6f7e7170 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 28 Jul 2019 21:09:19 -0400 Subject: [PATCH 11/12] tests: Test the gears.matcher greater and lesser sections. --- lib/gears/matcher.lua | 2 ++ tests/examples/text/gears/matcher/types.lua | 38 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index 300b26b4..a7f8b374 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -25,6 +25,8 @@ -- -- @DOC_text_gears_matcher_default_EXAMPLE@ -- +-- This example shows the different matching sections: +-- -- @DOC_text_gears_matcher_types_EXAMPLE@ -- -- More examples are available in `awful.rules`. diff --git a/tests/examples/text/gears/matcher/types.lua b/tests/examples/text/gears/matcher/types.lua index d4dd3c00..d82b64c0 100644 --- a/tests/examples/text/gears/matcher/types.lua +++ b/tests/examples/text/gears/matcher/types.lua @@ -4,6 +4,8 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE local matcher = gears.matcher() +--DOC_NEWLINE + matcher:append_rule( "my.source", { rule = { my_any_rule = true, @@ -21,6 +23,8 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE }, }) +--DOC_NEWLINE + local candidate1 = { my_any_rule = true, every1 = 1, @@ -60,3 +64,37 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE assert(candidate1.was_a_match == true ) assert(candidate2.was_a_match == false) assert(candidate3.was_a_match == false) + +--DOC_NEWLINE + + -- It is also possible to match number property by range. + matcher:append_rule( "my.source", { + rule_greater = { + value = 50, + }, + rule_lesser = { + value = 100, + }, + properties = { + was_a_match = true, + }, + }) + +--DOC_NEWLINE + + local candidate4 = { value = 40 , was_a_match = false } + local candidate5 = { value = 75 , was_a_match = false } + local candidate6 = { value = 101, was_a_match = false } + +--DOC_NEWLINE + + matcher:apply(candidate4) + matcher:apply(candidate5) + matcher:apply(candidate6) + +--DOC_NEWLINE + + -- Only candidate5 fits all criteria. + assert(candidate4.was_a_match == false) + assert(candidate5.was_a_match == true ) + assert(candidate6.was_a_match == false) From 67e5dd309153eb3ad8f3bd418ad995818b655edc Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Tue, 6 Aug 2019 01:50:18 -0400 Subject: [PATCH 12/12] matcher: Match all sources when none is provided. Nobody wants to set this parameter. It is necessary because the old API allowed `awful.rules` to be used with random for random matching. This stopped "really" working between the 3.4 and 3.5 release because the code started to accumulate "corner case" fixes aligned with the client properties. v4.0 added more ordering and v4.3 added external sources. After this, it is unusable with external objects, but `gears.matcher` handle this use case very well. --- lib/gears/matcher.lua | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua index a7f8b374..e389bf96 100644 --- a/lib/gears/matcher.lua +++ b/lib/gears/matcher.lua @@ -215,12 +215,21 @@ end -- -- @param o The object. -- @tparam[opt=nil] table rules The rules to check. List with "rule", "rule_any", --- "except" and "except_any" keys. If no rules are provided, one is selected at --- random. Unless more rule sources are added, there is only one to begin with. --- @treturn table The list of matched rules. +-- "except" and "except_any" keys. If no rules are provided, all rules +-- registered with a source will be matched. -- @method matching_rules function matcher:matching_rules(o, rules) - rules = rules or select(2, next(self._matching_rules)) + + -- Match all sources. + if not rules then + local ret = {} + + for _, r in pairs(self._matching_rules) do + gtable.merge(ret, self:matching_rules(o, r)) + end + + return ret + end local result = {}