gears: Extract the logic code from awful.rules into gears.matcher.

The use case for this is to reuse the matching logic for other objects
such as tags or notification.
This commit is contained in:
Emmanuel Lepage Vallee 2019-01-21 02:35:18 -05:00
parent 58bafe8ef4
commit 74c2e7382e
1 changed files with 318 additions and 0 deletions

318
lib/gears/matcher.lua Normal file
View File

@ -0,0 +1,318 @@
--- A module to build a set of properties based on a graph of rules.
--
-- Sources
-- =======
--
-- This module holds the business logic used by `awful.rules`. It provides an
-- object on which one can add sets of rules or, alternatively, functions.
-- In this module, the sets of rules or custom functions are called sources.
--
-- The sources are used to build a property table. Once all sources are
-- evaluated, the `:apply()` method will set the properties on the target
-- object.
--
-- Sources can have dependencies between them and the property table can only
-- be built if the sources graph can be resolved.
--
-- Rules
-- =====
--
-- The `rules` sources themselves are composed, as the name imply, of a set of
-- rule. A rule is a table with a `properties` *or* `callbacks` attribute along
-- with either `rule` or `rule_any`. It is also possible to add an `except` or
-- `except_any` attribute to narrow the scope in which the rule is applied.
-- Here's a basic example of a minimal `gears.matcher`.
--
-- @DOC_text_gears_matcher_default_EXAMPLE@
--
-- More examples are available in `awful.rules`.
--
-- @author Julien Danjou <julien@danjou.info>
-- @copyright 2009 Julien Danjou
-- @see awful.rules
-- @module gears.matcher
local gtable = require("gears.table")
local gsort = require("gears.sort")
local gdebug = require("gears.debug")
local protected_call = require("gears.protected_call")
local matcher = {}
-- 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)
if not rule then return false end
for field, value in pairs(rule) do
if o[field] 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
return false
end
end
return true
end
-- Check if an object matches any part of a rule.
-- @param o The object.
-- #tparam table rule The rule to 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
if o[field] == value then
return true
elseif type(o[field]) == "string" and o[field]:match(value) then
return true
end
end
end
end
return false
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
-- `except_any`).
-- @treturn boolean If `o` matches `entry`.
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))
end
--- Get list of matching rules for an object.
--
-- If the `rules` argument is not provided, the rules added with
-- `add_matching_rules` will be used.
--
-- @param o The object.
-- @tparam[opt=nil] table rules The rules to check. List with "rule", "rule_any",
-- "except" and "except_any" keys.
-- @treturn table The list of matched rules.
function matcher:matching_rules(o, rules)
local result = {}
for _, entry in ipairs(rules) do
if self:matches_rule(o, entry) then
table.insert(result, entry)
end
end
return result
end
--- Check if an object matches a given set of rules.
-- @param o The object.
-- @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.
function matcher:matches_rules(o, rules)
for _, entry in ipairs(rules) do
if self:matches_rule(o, entry) then
return true
end
end
return false
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 {})
if entry.callback then
table.insert(callbacks, entry.callback)
end
end
end
--- Add a set of matching rules.
--
-- @tparam string name The provider name. It must be unique.
-- @tparam table rules A set of rules (see how they work at the top of this
-- page).
-- @tparam[opt={}] table depends_on A list of names of sources this source
-- depends on (sources that must be executed *before* `name`).
-- @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.
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)
end
self._matching_rules[name] = rules
return self:add_matching_function(name, matching_fct, depends_on, precede)
end
--- Add a matching function.
--
-- @tparam string name The provider name. It must be unique.
-- @tparam function callback The callback that is called to produce properties.
-- @tparam gears.matcher callback.self The matcher object.
-- @param callback.o The object.
-- @tparam table callback.properties The current properties. The callback should
-- add to and overwrite properties in this table.
-- @tparam table callback.callbacks A table of all callbacks scheduled to be
-- executed after the main properties are applied.
-- @tparam[opt={}] table depends_on A list of names of sources this source depends on
-- (sources that must be executed *before* `name`).
-- @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.
function matcher:add_matching_function(name, callback, depends_on, precede)
depends_on = depends_on or {}
precede = precede or {}
assert(type( depends_on ) == "table")
assert(type( precede ) == "table")
for _, v in ipairs(self._matching_source) do
-- Names must be unique
assert(
v.name ~= name,
"Name must be unique, but '" .. name .. "' was already registered."
)
end
local new_sources = self._rule_source_sort:clone()
new_sources:prepend(name, precede )
new_sources:append (name, depends_on )
local res, err = new_sources:sort()
if err then
gdebug.print_warning("Failed to add the rule source: "..err)
return false
end
-- Only replace the source once the additions has been proven to be safe.
self._rule_source_sort = new_sources
local callbacks = {}
-- Get all callbacks for *existing* sources.
-- It is important to remember that names can be used in the sorting even
-- if the source itself doesn't (yet) exist.
for _, v in ipairs(self._matching_source) do
callbacks[v.name] = v.callback
end
self._matching_source = {}
callbacks[name] = callback
for _, v in ipairs(res) do
if callbacks[v] then
table.insert(self._matching_source, 1, {
callback = callbacks[v],
name = v
})
end
end
return true
end
--- Remove a source.
--
-- This removes sources added with `add_matching_function` or
-- `add_matching_rules`.
--
-- @tparam string name The source name.
-- @treturn boolean If the source has been removed.
function matcher:remove_matching_source(name)
self._rule_source_sort:remove(name)
for k, v in ipairs(self._matching_source) do
if v.name == name then
table.remove(self._matching_source, k)
return true
end
end
self._matching_rules[name] = nil
return false
end
--- Apply awful.rules.rules to an object.
--
-- Calling this will apply all properties provided by the matching functions
-- and rules.
--
-- @param o The object.
function matcher:apply(o)
local callbacks, props = {}, {}
for _, v in ipairs(self._matching_source) do
v.callback(self, o, props, callbacks)
end
self:_execute(o, props, callbacks)
end
-- Execute the rules for the object `o`.
-- @param o The object.
-- @tparam table props A list of properties to apply.
-- @tparam table callbacks A list of callback to execute with the object `o` as
-- sole argument. The callbacks are executed *before* applying the properties.
-- @see gears.matcher.apply
function matcher:_execute(o, props, callbacks)
-- Apply all callbacks.
if callbacks then
for _, callback in pairs(callbacks) do
protected_call(callback, o)
end
end
for property, value in pairs(props) do
if type(value) == "function" then
value = value(o, props)
end
if type(o[property]) == "function" then
o[property](o, value)
else
o[property] = value
end
end
end
local module = {}
--- Create a new rule solver object.
-- @function gears.matcher
-- @return A new rule solver object.
local function new()
local ret = {}
-- Contains the sources.
-- The elements are ordered "first in, first executed". Thus, the higher the
-- index, the higher the priority. Each entry is a table with a `name` and a
-- `callback` field. This table is exposed for debugging purpose. The API
-- is private and should only be modified using the public accessors.
ret._matching_source = {}
ret._rule_source_sort = gsort.topological()
ret._matching_rules = {}
gtable.crush(ret, matcher, true)
return ret
end
--@DOC_rule_COMMON@
--@DOC_object_COMMON@
return setmetatable(module, {__call = new})