rules: Add a rule building chain of responsability

Before this commit, there was a conflict between the spawn and
awful.rules rules.

Also, modules such as Tryannical monkey-patched this function to
add their own rules to the mix. This commit introduce a proper
API to add handlers.

The order is crutial for this to work, so a dependency system is
also added.

Fix #1482
This commit is contained in:
Emmanuel Lepage Vallee 2017-01-28 16:12:39 -05:00
parent 20db37f892
commit 74508098de
3 changed files with 174 additions and 12 deletions

View file

@ -66,6 +66,8 @@ new_type("legacylayout", "Layout related functions", false, "param")
new_type("clientlayout", "Client layouts", false, "param") new_type("clientlayout", "Client layouts", false, "param")
-- Document some callback prototypes -- Document some callback prototypes
new_type("callback", "Callback functions prototype", false, "Parameters") new_type("callback", "Callback functions prototype", false, "Parameters")
-- awful.rules sources
new_type("rulesources", "Rule sources", false, "param")
-- More fitting section names -- More fitting section names
kind_names={topic='Documentation', module='Libraries', script='Sample files'} kind_names={topic='Documentation', module='Libraries', script='Sample files'}

View file

@ -37,6 +37,10 @@ local atag = require("awful.tag")
local gtable = require("gears.table") local gtable = require("gears.table")
local a_place = require("awful.placement") local a_place = require("awful.placement")
local protected_call = require("gears.protected_call") local protected_call = require("gears.protected_call")
local aspawn = require("awful.spawn")
local gsort = require("gears.sort")
local gdebug = require("gears.debug")
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local rules = {} local rules = {}
@ -207,23 +211,176 @@ function rules.matches_list(c, _rules)
return false return false
end end
--- Apply awful.rules.rules to a client.
-- @client c The client.
function rules.apply(c)
local props = {} -- 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 be modified using the public accessors.
local rule_sources = {}
local rule_source_sort = gsort.topological()
--- Add a new rule source.
--
-- A rule source is a provider called when a client is managed (started). It
-- allows to configure the client by providing properties that should be applied.
-- By default, Awesome provides 2 sources:
--
-- * `awful.rules`: A declarative matcher
-- * `awful.spawn`: Launch clients with pre-defined properties
--
-- It is possible to register new callbacks to modify the properties table
-- before it is applied. Each provider is executed sequentially and modifies the
-- same table. If the first provider set a property, then the second can
-- override it, then the third, etc. Once the providers are exhausted, the
-- properties are applied on the client.
--
-- It is important to note that properties themselves have their own
-- dependencies. For example, a `tag` property implies a `screen`. Therefor, if
-- a `screen` is already specified, then it will be ignored when the rule is
-- executed. Properties also have their own priorities. For example, the
-- `titlebar` and `border_width` need to be applied before the `x` and `y`
-- positions are set. Otherwise, it will be off or the client will shift
-- upward everytime Awesome is restarted. A rule source *cannot* change this.
-- It is up to the callback to be aware of the dependencies and avoid to
-- introduce issues. For example, if the source wants to set a `screen`, it has
-- to check if the `tag`, `tags` or `new_tag` are on that `screen` or remove
-- those properties. Otherwise, they will be ignored once the rule is applied.
--
-- @tparam string name The provider name. It must be unique.
-- @tparam function callback The callback that is called to produce properties.
-- @tparam client callback.c The client
-- @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 have a
-- priority over.
-- @treturn boolean Returns false if a dependency conflict was found.
function rules.add_rule_source(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(rule_sources) do
-- Names must be unique
assert(
v.name ~= name,
"Name must be unique, but '" .. name .. "' was already registered."
)
end
local new_sources = 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 have been proven safe
rule_source_sort = new_sources
local callbacks = {} local callbacks = {}
for _, entry in ipairs(rules.matching_rules(c, rules.rules)) do -- Get all callbacks for *existing* sources.
if entry.properties then -- It is important to remember that names can be used in the sorting even
for property, value in pairs(entry.properties) do -- if the source itself doesn't (yet) exists.
props[property] = value for _, v in ipairs(rule_sources) do
end callbacks[v.name] = v.callback
end
rule_sources = {}
callbacks[name] = callback
for _, v in ipairs(res) do
if callbacks[v] then
table.insert(rule_sources, 1, {
callback = callbacks[v],
name = v
})
end end
end
return true
end
--- Remove a source.
-- @tparam string name The source name.
-- @treturn boolean If the source was removed
function rules.remove_rule_source(name)
rule_source_sort:remove(name)
for k, v in ipairs(rule_sources) do
if v.name == name then
table.remove(rule_sources, k)
return true
end
end
return false
end
-- Add the rules properties
local function apply_awful_rules(c, props, callbacks)
for _, entry in ipairs(rules.matching_rules(c, rules.rules)) do
gtable.crush(props,entry.properties or {})
if entry.callback then if entry.callback then
table.insert(callbacks, entry.callback) table.insert(callbacks, entry.callback)
end end
end end
end
--- The default `awful.rules` source.
--
-- **Has priority over:**
--
-- *nothing*
--
-- @rulesources awful.rules
rules.add_rule_source("awful.rules", apply_awful_rules, {"awful.spawn"}, {})
-- Add startup_id overridden properties
local function apply_spawn_rules(c, props, callbacks)
if c.startup_id and aspawn.snid_buffer[c.startup_id] then
local snprops, sncb = unpack(aspawn.snid_buffer[c.startup_id])
-- The SNID tag(s) always have precedence over the rules one(s)
if snprops.tag or snprops.tags or snprops.new_tag then
props.tag, props.tags, props.new_tag = nil, nil, nil
end
gtable.crush(props, snprops)
gtable.merge(callbacks, sncb)
end
end
--- The rule source for clients spawned by `awful.spawn`.
--
-- **Has priority over:**
--
-- * `awful.rules`
--
-- @rulesources awful.spawn
rules.add_rule_source("awful.spawn", apply_spawn_rules, {}, {"awful.rules"})
--- Apply awful.rules.rules to a client.
-- @client c The client.
function rules.apply(c)
local callbacks, props = {}, {}
for _, v in ipairs(rule_sources) do
v.callback(c, props, callbacks)
end
rules.execute(c, props, callbacks) rules.execute(c, props, callbacks)
end end
@ -531,12 +688,11 @@ function rules.execute(c, props, callbacks)
end end
end end
-- TODO v5 deprecate this
function rules.completed_with_payload_callback(c, props, callbacks) function rules.completed_with_payload_callback(c, props, callbacks)
rules.execute(c, props, callbacks) rules.execute(c, props, callbacks)
end end
client.connect_signal("spawn::completed_with_payload", rules.completed_with_payload_callback)
client.connect_signal("manage", rules.apply) client.connect_signal("manage", rules.apply)
return rules return rules

View file

@ -165,6 +165,7 @@ local lgi = require("lgi")
local Gio = lgi.Gio local Gio = lgi.Gio
local GLib = lgi.GLib local GLib = lgi.GLib
local util = require("awful.util") local util = require("awful.util")
local timer = require("gears.timer")
local protected_call = require("gears.protected_call") local protected_call = require("gears.protected_call")
local spawn = {} local spawn = {}
@ -217,7 +218,10 @@ function spawn.on_snid_callback(c)
local props = entry[1] local props = entry[1]
local callback = entry[2] local callback = entry[2]
c:emit_signal("spawn::completed_with_payload", props, callback) c:emit_signal("spawn::completed_with_payload", props, callback)
spawn.snid_buffer[c.startup_id] = nil
timer.delayed_call(function()
spawn.snid_buffer[c.startup_id] = nil
end)
end end
end end