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:
parent
58bafe8ef4
commit
74c2e7382e
|
@ -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})
|
Loading…
Reference in New Issue