diff --git a/lib/gears/matcher.lua b/lib/gears/matcher.lua new file mode 100644 index 00000000..fac9ffc1 --- /dev/null +++ b/lib/gears/matcher.lua @@ -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})